@namzu/sdk 39.0.0 → 40.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 +151 -0
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +20 -6
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +8 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +12 -0
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +11 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +14 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +2 -3
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/registry/tool/portable.d.ts +65 -0
- package/dist/registry/tool/portable.d.ts.map +1 -0
- package/dist/registry/tool/portable.js +244 -0
- package/dist/registry/tool/portable.js.map +1 -0
- package/dist/registry/tool/schema.d.ts +32 -5
- package/dist/registry/tool/schema.d.ts.map +1 -1
- package/dist/registry/tool/schema.js +35 -9
- package/dist/registry/tool/schema.js.map +1 -1
- package/dist/registry/toolset/catalog.js +8 -8
- package/dist/registry/toolset/catalog.js.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.d.ts +215 -0
- package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -0
- package/dist/runtime/jobs/awaited-jobs.js +259 -0
- package/dist/runtime/jobs/awaited-jobs.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +33 -2
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +37 -0
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +28 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +39 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/file-evidence-context.d.ts.map +1 -1
- package/dist/runtime/query/file-evidence-context.js +159 -43
- package/dist/runtime/query/file-evidence-context.js.map +1 -1
- package/dist/runtime/query/file-evidence-replay.d.ts +260 -0
- package/dist/runtime/query/file-evidence-replay.d.ts.map +1 -0
- package/dist/runtime/query/file-evidence-replay.js +647 -0
- package/dist/runtime/query/file-evidence-replay.js.map +1 -0
- package/dist/runtime/query/file-evidence-seed.d.ts +50 -0
- package/dist/runtime/query/file-evidence-seed.d.ts.map +1 -0
- package/dist/runtime/query/file-evidence-seed.js +100 -0
- package/dist/runtime/query/file-evidence-seed.js.map +1 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +94 -2
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +87 -9
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +193 -28
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +5 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/plugin-hooks.d.ts +14 -0
- package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +18 -0
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/repeat-call.d.ts +17 -4
- package/dist/runtime/query/repeat-call.d.ts.map +1 -1
- package/dist/runtime/query/repeat-call.js +26 -19
- package/dist/runtime/query/repeat-call.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +11 -1
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +12 -1
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts +48 -2
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +102 -10
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +4 -10
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/edit-apply.d.ts +126 -0
- package/dist/tools/builtins/edit-apply.d.ts.map +1 -0
- package/dist/tools/builtins/edit-apply.js +360 -0
- package/dist/tools/builtins/edit-apply.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts +143 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +37 -219
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/index.d.ts +1 -0
- package/dist/tools/builtins/index.d.ts.map +1 -1
- package/dist/tools/builtins/index.js +9 -3
- package/dist/tools/builtins/index.js.map +1 -1
- package/dist/tools/builtins/job.js +1 -1
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +2 -2
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +50 -65
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-render.d.ts +56 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -0
- package/dist/tools/builtins/read-render.js +73 -0
- package/dist/tools/builtins/read-render.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +67 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.js +108 -0
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -0
- package/dist/tools/builtins/wait-for-job.d.ts +6 -0
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -0
- package/dist/tools/builtins/wait-for-job.js +162 -0
- package/dist/tools/builtins/wait-for-job.js.map +1 -0
- package/dist/tools/builtins/write-file.js +5 -0
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +1 -7
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +88 -10
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/run/entity.d.ts +13 -0
- package/dist/types/run/entity.d.ts.map +1 -1
- package/dist/types/sandbox/index.d.ts +15 -14
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +109 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/utils/env.d.ts +19 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +25 -0
- package/dist/utils/env.js.map +1 -0
- package/package.json +1 -1
- package/src/connector/mcp/adapter.ts +20 -6
- package/src/manager/run/persistence.ts +12 -0
- package/src/public-runtime.ts +9 -1
- package/src/public-tools.ts +18 -0
- package/src/registry/tool/execute.ts +2 -4
- package/src/registry/tool/portable.ts +264 -0
- package/src/registry/tool/schema.ts +38 -8
- package/src/registry/toolset/catalog.ts +8 -9
- package/src/runtime/jobs/awaited-jobs.ts +271 -0
- package/src/runtime/jobs/registry.ts +50 -0
- package/src/runtime/query/executor.ts +49 -1
- package/src/runtime/query/file-evidence-context.ts +190 -46
- package/src/runtime/query/file-evidence-replay.ts +776 -0
- package/src/runtime/query/file-evidence-seed.ts +126 -0
- package/src/runtime/query/index.ts +104 -2
- package/src/runtime/query/iteration/index.ts +202 -28
- package/src/runtime/query/iteration/phases/context.ts +10 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +4 -0
- package/src/runtime/query/plugin-hooks.ts +20 -0
- package/src/runtime/query/repeat-call.ts +28 -18
- package/src/runtime/query/steering.ts +11 -0
- package/src/runtime/query/tooling.ts +3 -0
- package/src/scheduler/completion-inbox.ts +105 -9
- package/src/tools/builtins/bash.ts +4 -10
- package/src/tools/builtins/edit-apply.ts +456 -0
- package/src/tools/builtins/edit.ts +39 -270
- package/src/tools/builtins/index.ts +9 -3
- package/src/tools/builtins/job.ts +1 -1
- package/src/tools/builtins/read-file.ts +56 -77
- package/src/tools/builtins/read-render.ts +104 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +179 -0
- package/src/tools/builtins/wait-for-job.ts +184 -0
- package/src/tools/builtins/write-file.ts +5 -0
- package/src/tools/coordinator/index.ts +1 -7
- package/src/tools/file-read-tracker.ts +85 -7
- package/src/types/message/index.ts +2 -0
- package/src/types/run/entity.ts +14 -0
- package/src/types/sandbox/index.ts +15 -14
- package/src/types/tool/index.ts +104 -0
- package/src/utils/env.ts +23 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema shape every wire namzu speaks reads the same way.
|
|
3
|
+
*
|
|
4
|
+
* `dialect.ts` solves the problem one wire at a time: render draft-07, and let
|
|
5
|
+
* each driver say it again in the dialect it knows its endpoint parses. That
|
|
6
|
+
* works only where a driver HAS a measurement. Of the ten driver packages,
|
|
7
|
+
* three convert; the other seven forward the rendering verbatim, because
|
|
8
|
+
* nobody had measured their wires.
|
|
9
|
+
*
|
|
10
|
+
* One of those seven then got measured, the hard way. Its gateway validates a
|
|
11
|
+
* tool's `parameters` against the JSON Schema 2020-12 metaschema, and `read`'s
|
|
12
|
+
* `readRange` — the only `z.tuple(...)` in the first-party tool surface —
|
|
13
|
+
* renders draft-07 as `items: [a, b]`:
|
|
14
|
+
*
|
|
15
|
+
* [400] Tool 4 function has invalid 'parameters' schema:
|
|
16
|
+
* [{'minimum': 1, 'type': 'integer'}, {'minimum': 1, 'type': 'integer'}]
|
|
17
|
+
* is not of type 'object', 'boolean'
|
|
18
|
+
*
|
|
19
|
+
* The lesson is not "convert in five more drivers". Wiring each driver to a
|
|
20
|
+
* dialect requires a measurement per wire, and the two hardest to measure are
|
|
21
|
+
* precisely the ones nobody can measure on a user's behalf: the driver whose
|
|
22
|
+
* schema field the vendor documents nowhere, and every compatible endpoint a
|
|
23
|
+
* user points the generic HTTP driver at.
|
|
24
|
+
*
|
|
25
|
+
* So the kernel emits the INTERSECTION instead: the subset of JSON Schema that
|
|
26
|
+
* is valid, and means the same thing, in draft-07 and in 2020-12 alike. A
|
|
27
|
+
* schema in that subset needs no conversion anywhere, which is why this runs at
|
|
28
|
+
* the rendering boundary rather than at ten driver boundaries.
|
|
29
|
+
*
|
|
30
|
+
* `toSchemaDialect` is not replaced by this and does not become dead: a driver
|
|
31
|
+
* can still be handed a `parameters` object namzu never rendered (a host
|
|
32
|
+
* passing `ChatCompletionParams` straight in), and converting at the boundary
|
|
33
|
+
* remains the right answer there.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export interface PortableSchemaViolation {
|
|
37
|
+
/** Dotted path to the offending keyword, e.g. `properties.readRange.items`. */
|
|
38
|
+
readonly path: string
|
|
39
|
+
readonly keyword: string
|
|
40
|
+
/** What to write instead. */
|
|
41
|
+
readonly remedy: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const NO_TUPLES =
|
|
45
|
+
'say the array uniformly — one `items` schema plus `minItems`/`maxItems`; a tuple is spelled differently in draft-07 and 2020-12 and is invalid in one of them either way'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Keywords outside the intersection, and why each one is out.
|
|
49
|
+
*
|
|
50
|
+
* A deny-list, like `findStrictSchemaViolations` and for the same reason: an
|
|
51
|
+
* allow-list would have to enumerate every annotation a schema may carry and
|
|
52
|
+
* would refuse a schema for saying something harmless.
|
|
53
|
+
*
|
|
54
|
+
* Every entry names a wire in the driver matrix that reads it wrong, not a
|
|
55
|
+
* style preference.
|
|
56
|
+
*/
|
|
57
|
+
const OUTSIDE_THE_INTERSECTION: ReadonlyMap<string, string> = new Map([
|
|
58
|
+
// 2020-12 moved the positional schemas to `prefixItems` and kept `items`
|
|
59
|
+
// for the tail, so an array-valued `items` does not merely look old — it
|
|
60
|
+
// means something else, and a 2020-12 validator rejects the whole request.
|
|
61
|
+
// Measured on Zen's Console gateway; the quote is in this file's header.
|
|
62
|
+
['additionalItems', NO_TUPLES],
|
|
63
|
+
// The other spelling of the same tuple. A draft-07 validator does not know
|
|
64
|
+
// the keyword, so it ignores it — which turns a constrained array into an
|
|
65
|
+
// unconstrained one SILENTLY, the worse failure of the two. The strict
|
|
66
|
+
// tool-input subset refuses it outright as well (`strict-schema.ts`), and
|
|
67
|
+
// the function-declaration schema on one of these wires is an OpenAPI 3.0
|
|
68
|
+
// subset that has never documented it.
|
|
69
|
+
['prefixItems', NO_TUPLES],
|
|
70
|
+
// A rendered tool schema has to stand alone. The renderer already runs with
|
|
71
|
+
// `$refStrategy: 'none'`, and the MCP bridge inlines refs before handing a
|
|
72
|
+
// schema out, because a wire that does not resolve them sees an empty
|
|
73
|
+
// constraint or refuses the document.
|
|
74
|
+
['$ref', 'inline the referenced schema; a tool schema has to stand alone'],
|
|
75
|
+
['$defs', 'inline the definitions; a tool schema has to stand alone'],
|
|
76
|
+
['definitions', 'inline the definitions; a tool schema has to stand alone'],
|
|
77
|
+
['$id', 'drop it; a tool schema is not a retrievable document'],
|
|
78
|
+
['$anchor', 'drop it; there are no references to anchor'],
|
|
79
|
+
['$dynamicRef', 'inline the referenced schema; a tool schema has to stand alone'],
|
|
80
|
+
['$dynamicAnchor', 'drop it; there are no references to anchor'],
|
|
81
|
+
// Nothing on any wire reads it, and asserting a dialect is the one thing a
|
|
82
|
+
// schema that must work in both must not do. It also rides in the tools
|
|
83
|
+
// block, which renders at position 0 inside the prompt-cache prefix.
|
|
84
|
+
['$schema', 'drop it; no provider reads it and it asserts a dialect'],
|
|
85
|
+
// 2019-09 vocabulary. Same silent-widening hazard as `prefixItems` on a
|
|
86
|
+
// draft-07 validator.
|
|
87
|
+
['unevaluatedItems', 'state the constraint with `items` and `maxItems`'],
|
|
88
|
+
['unevaluatedProperties', 'state the constraint with `additionalProperties`'],
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Every place a schema leaves the intersection, with its exact path.
|
|
93
|
+
*
|
|
94
|
+
* Exported so a test can sweep every shipped tool and a driver can assert
|
|
95
|
+
* rather than hope. The path is the point: the vendor's error names the tool
|
|
96
|
+
* index and the offending fragment but never where inside the schema it sits.
|
|
97
|
+
*/
|
|
98
|
+
export function findPortableSchemaViolations(
|
|
99
|
+
schema: unknown,
|
|
100
|
+
path = '',
|
|
101
|
+
): PortableSchemaViolation[] {
|
|
102
|
+
if (Array.isArray(schema)) {
|
|
103
|
+
return schema.flatMap((item, index) => findPortableSchemaViolations(item, `${path}[${index}]`))
|
|
104
|
+
}
|
|
105
|
+
if (typeof schema !== 'object' || schema === null) return []
|
|
106
|
+
|
|
107
|
+
const found: PortableSchemaViolation[] = []
|
|
108
|
+
for (const [keyword, value] of Object.entries(schema as Record<string, unknown>)) {
|
|
109
|
+
const here = path ? `${path}.${keyword}` : keyword
|
|
110
|
+
const remedy = OUTSIDE_THE_INTERSECTION.get(keyword)
|
|
111
|
+
if (remedy !== undefined) {
|
|
112
|
+
found.push({ path: here, keyword, remedy })
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
// The draft-07 tuple. Reported once, at the tuple, rather than once per
|
|
116
|
+
// positional schema underneath it — one fixable finding, not a list.
|
|
117
|
+
if (keyword === 'items' && Array.isArray(value)) {
|
|
118
|
+
found.push({ path: here, keyword, remedy: NO_TUPLES })
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
// A union of types is valid JSON Schema in both dialects, but the
|
|
122
|
+
// OpenAPI-3.0-shaped wires namzu speaks take a single type name and
|
|
123
|
+
// either refuse a list or ignore it.
|
|
124
|
+
if (keyword === 'type' && Array.isArray(value)) {
|
|
125
|
+
found.push({
|
|
126
|
+
path: here,
|
|
127
|
+
keyword,
|
|
128
|
+
remedy: 'use `anyOf` of single-typed schemas; several wires take one type name only',
|
|
129
|
+
})
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
found.push(...findPortableSchemaViolations(value, here))
|
|
133
|
+
}
|
|
134
|
+
return found
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Rewrite what can be rewritten, so a tuple cannot reach a wire.
|
|
139
|
+
*
|
|
140
|
+
* Only the tuple spellings and `$schema` are rewritten. The rest of the
|
|
141
|
+
* deny-list is reported and not repaired on purpose: inlining a `$ref` or
|
|
142
|
+
* choosing which member of a `type` list the author meant is a guess about
|
|
143
|
+
* intent, and a guess that silently changes what a tool accepts is worse than
|
|
144
|
+
* the 400 it avoids. Those are caught by the profile test at the source.
|
|
145
|
+
*
|
|
146
|
+
* Returns the input unchanged — the SAME reference — when nothing needs
|
|
147
|
+
* rewriting, which is the overwhelming common case. The tools block sits at
|
|
148
|
+
* position 0 of the prompt-cache prefix, so a fresh, equal object per request
|
|
149
|
+
* would invalidate the cache for the whole run.
|
|
150
|
+
*/
|
|
151
|
+
export function toPortableToolSchema(json: Record<string, unknown>): Record<string, unknown> {
|
|
152
|
+
return makePortable(json) as Record<string, unknown>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function makePortable(value: unknown): unknown {
|
|
156
|
+
if (Array.isArray(value)) {
|
|
157
|
+
let changed = false
|
|
158
|
+
const out = value.map((item) => {
|
|
159
|
+
const next = makePortable(item)
|
|
160
|
+
if (next !== item) changed = true
|
|
161
|
+
return next
|
|
162
|
+
})
|
|
163
|
+
return changed ? out : value
|
|
164
|
+
}
|
|
165
|
+
if (typeof value !== 'object' || value === null) return value
|
|
166
|
+
|
|
167
|
+
const node = value as Record<string, unknown>
|
|
168
|
+
const positional = Array.isArray(node.prefixItems)
|
|
169
|
+
? node.prefixItems
|
|
170
|
+
: Array.isArray(node.items)
|
|
171
|
+
? node.items
|
|
172
|
+
: undefined
|
|
173
|
+
if (positional) return collapseTuple(node, positional)
|
|
174
|
+
|
|
175
|
+
let changed = '$schema' in node
|
|
176
|
+
const out: Record<string, unknown> = {}
|
|
177
|
+
for (const [key, child] of Object.entries(node)) {
|
|
178
|
+
if (key === '$schema') continue
|
|
179
|
+
const next = makePortable(child)
|
|
180
|
+
if (next !== child) changed = true
|
|
181
|
+
out[key] = next
|
|
182
|
+
}
|
|
183
|
+
return changed ? out : node
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Say a positional array uniformly.
|
|
188
|
+
*
|
|
189
|
+
* What is kept: the arity, and every constraint that applies to every member.
|
|
190
|
+
* What is lost: which member sits where. That loss is real and it is the price
|
|
191
|
+
* of a shape both dialects read identically — and it costs nothing in practice,
|
|
192
|
+
* because the tool's Zod schema still parses the call and still refuses a
|
|
193
|
+
* wrongly-ordered array with a message the model can act on. The wire schema is
|
|
194
|
+
* a hint to the model; the parser is the contract.
|
|
195
|
+
*/
|
|
196
|
+
function collapseTuple(
|
|
197
|
+
node: Record<string, unknown>,
|
|
198
|
+
positional: readonly unknown[],
|
|
199
|
+
): Record<string, unknown> {
|
|
200
|
+
// The tail rule, in whichever dialect this node happens to be written in:
|
|
201
|
+
// draft-07 spells it `additionalItems`, 2020-12 spells it `items` and only
|
|
202
|
+
// calls it a tail when `prefixItems` holds the members.
|
|
203
|
+
const tail = Array.isArray(node.prefixItems) ? node.items : node.additionalItems
|
|
204
|
+
const arity = positional.length
|
|
205
|
+
const members = positional.map(makePortable)
|
|
206
|
+
|
|
207
|
+
// A tail that is itself a schema constrains every element past the members,
|
|
208
|
+
// so those elements join the uniform reading rather than closing it.
|
|
209
|
+
const tailSchema =
|
|
210
|
+
tail !== undefined && tail !== true && tail !== false ? makePortable(tail) : undefined
|
|
211
|
+
// `false` closes the array at the members' length; so does a `maxItems` the
|
|
212
|
+
// author already pinned there.
|
|
213
|
+
const closed =
|
|
214
|
+
tailSchema === undefined &&
|
|
215
|
+
(tail === false || (typeof node.maxItems === 'number' && node.maxItems <= arity))
|
|
216
|
+
|
|
217
|
+
// An open tail with no schema says "anything past the members", which no
|
|
218
|
+
// single `items` can express alongside the members themselves. Saying
|
|
219
|
+
// nothing about the elements is the only honest uniform reading; the bounds
|
|
220
|
+
// below still carry whatever the author pinned.
|
|
221
|
+
const candidates =
|
|
222
|
+
tailSchema !== undefined ? dedupe([...members, tailSchema]) : closed ? dedupe(members) : []
|
|
223
|
+
|
|
224
|
+
const element =
|
|
225
|
+
candidates.length === 0
|
|
226
|
+
? undefined
|
|
227
|
+
: candidates.length === 1
|
|
228
|
+
? candidates[0]
|
|
229
|
+
: { anyOf: candidates }
|
|
230
|
+
|
|
231
|
+
const out: Record<string, unknown> = {}
|
|
232
|
+
for (const [key, child] of Object.entries(node)) {
|
|
233
|
+
if (key === '$schema') continue
|
|
234
|
+
if (key === 'additionalItems') continue
|
|
235
|
+
if (key === 'prefixItems' || (key === 'items' && Array.isArray(child))) {
|
|
236
|
+
if (element !== undefined) out.items = element
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
// The 2020-12 tail, already folded into `items` above.
|
|
240
|
+
if (key === 'items' && Array.isArray(node.prefixItems)) continue
|
|
241
|
+
if (key === 'maxItems' && closed && typeof child === 'number') {
|
|
242
|
+
out.maxItems = Math.min(child, arity)
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
out[key] = makePortable(child)
|
|
246
|
+
}
|
|
247
|
+
// A tuple closed by `additionalItems: false` rather than by a bound loses
|
|
248
|
+
// its ceiling otherwise: a uniform `items` says nothing about length, so a
|
|
249
|
+
// schema written to forbid a third element would begin to allow any number.
|
|
250
|
+
if (closed && out.maxItems === undefined) out.maxItems = arity
|
|
251
|
+
return out
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function dedupe(schemas: readonly unknown[]): unknown[] {
|
|
255
|
+
const seen = new Set<string>()
|
|
256
|
+
const out: unknown[] = []
|
|
257
|
+
for (const schema of schemas) {
|
|
258
|
+
const key = JSON.stringify(schema) ?? 'undefined'
|
|
259
|
+
if (seen.has(key)) continue
|
|
260
|
+
seen.add(key)
|
|
261
|
+
out.push(schema)
|
|
262
|
+
}
|
|
263
|
+
return out
|
|
264
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { z } from 'zod'
|
|
2
2
|
import { zodToJsonSchema } from 'zod-to-json-schema'
|
|
3
|
+
import { toPortableToolSchema } from './portable.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The single place a tool's Zod schema becomes the JSON Schema that goes on
|
|
@@ -50,17 +51,46 @@ export function renderToolSchema(schema: z.ZodType): Record<string, unknown> {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
|
-
*
|
|
54
|
+
* Make the rendering something every wire reads the same way.
|
|
54
55
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* Two jobs, both at this one boundary:
|
|
57
|
+
*
|
|
58
|
+
* 1. **Strip what no provider reads.** `$schema` rides in the tools block,
|
|
59
|
+
* which renders at position 0 inside the prompt-cache prefix, once per
|
|
60
|
+
* tool, per request, forever — and it asserts a dialect, which is the one
|
|
61
|
+
* thing a schema that has to work on every wire must not do.
|
|
62
|
+
*
|
|
63
|
+
* 2. **Leave the intersection of draft-07 and 2020-12 intact.** `renderToolSchema`
|
|
64
|
+
* emits draft-07, where a tuple is `items: [a, b]`; a 2020-12 wire reads
|
|
65
|
+
* that as invalid and rejects the whole request, taking every other tool in
|
|
66
|
+
* the call down with it. Five of the ten drivers forward the rendering
|
|
67
|
+
* verbatim, so the only place that can be fixed once is here. See
|
|
68
|
+
* `portable.ts` for the measurement that put it here.
|
|
59
69
|
*/
|
|
60
70
|
export function normalizeToolSchema(json: Record<string, unknown>): Record<string, unknown> {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
return toPortableToolSchema(json)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The schema a tool actually puts on the wire.
|
|
76
|
+
*
|
|
77
|
+
* A tool may carry an explicit `modelInputSchema` — hand-written JSON Schema,
|
|
78
|
+
* not rendered from Zod — and that schema reaches providers through the same
|
|
79
|
+
* `tools` block as a rendered one. It therefore has to clear the same bar, and
|
|
80
|
+
* the two call sites that build that block (`ToolRegistry.toLLMTools` and the
|
|
81
|
+
* toolset catalog) had each spelled the fallback out on their own. Two paths
|
|
82
|
+
* that agree today are two paths that can disagree tomorrow.
|
|
83
|
+
*
|
|
84
|
+
* The clone is deliberate: a caller may edit the parameters it is handed —
|
|
85
|
+
* `hostModelSchema` does — and the definition's own object must not move
|
|
86
|
+
* underneath the next call.
|
|
87
|
+
*/
|
|
88
|
+
export function toolWireSchema(tool: {
|
|
89
|
+
readonly modelInputSchema?: Record<string, unknown>
|
|
90
|
+
readonly inputSchema: z.ZodType
|
|
91
|
+
}): Record<string, unknown> {
|
|
92
|
+
if (tool.modelInputSchema) return toPortableToolSchema(structuredClone(tool.modelInputSchema))
|
|
93
|
+
return renderToolSchema(tool.inputSchema)
|
|
64
94
|
}
|
|
65
95
|
|
|
66
96
|
/**
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
ToolsetDefinition,
|
|
10
10
|
ToolsetPolicy,
|
|
11
11
|
} from '../../types/toolset/index.js'
|
|
12
|
-
import {
|
|
12
|
+
import { toolWireSchema } from '../tool/schema.js'
|
|
13
13
|
|
|
14
14
|
export interface ToolCatalogSearchOptions {
|
|
15
15
|
readonly loading?: readonly ToolLoadingMode[]
|
|
@@ -245,17 +245,16 @@ function toolDefinitionToLLMTool(definition: ToolDefinition | undefined): LLMToo
|
|
|
245
245
|
function: {
|
|
246
246
|
name: definition.name,
|
|
247
247
|
description: definition.description,
|
|
248
|
-
// Through `
|
|
248
|
+
// Through `toolWireSchema`, not a second inline conversion. The
|
|
249
249
|
// options were already identical, so this was not a different
|
|
250
250
|
// rendering — it was the same rendering without the guarantees:
|
|
251
251
|
// no `$schema` stripping (that key rides in the tools block, which
|
|
252
|
-
// sits at position 0 inside the prompt-cache prefix), no
|
|
253
|
-
// and no freeze. Two paths that agree
|
|
254
|
-
// disagree tomorrow, and a tool reaching
|
|
255
|
-
// rather than the registry is not a
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
renderToolSchema(definition.inputSchema),
|
|
252
|
+
// sits at position 0 inside the prompt-cache prefix), no portability
|
|
253
|
+
// normalisation, no memoization, and no freeze. Two paths that agree
|
|
254
|
+
// today are two paths that can disagree tomorrow, and a tool reaching
|
|
255
|
+
// the wire through the catalog rather than the registry is not a
|
|
256
|
+
// different tool.
|
|
257
|
+
parameters: toolWireSchema(definition),
|
|
259
258
|
},
|
|
260
259
|
}
|
|
261
260
|
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { BackgroundJob, BackgroundJobRegistry } from './registry.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What this adapter needs from a registry: the jobs' exits, and one job's
|
|
5
|
+
* current status. Structural rather than the class, for the reason
|
|
6
|
+
* `BackgroundJobRegistryRef` is: the kernel passes the real registry, and a
|
|
7
|
+
* test that needs two exits and nothing else should not have to spawn
|
|
8
|
+
* processes to produce them.
|
|
9
|
+
*/
|
|
10
|
+
export type AwaitedJobSource = Pick<BackgroundJobRegistry, 'get' | 'onExit'>
|
|
11
|
+
|
|
12
|
+
/** Exits and the text that puts them in front of the model. See {@link AwaitedJobs.takeDelivery}. */
|
|
13
|
+
export interface DeliverableJobExits {
|
|
14
|
+
/** The notice text, already queued by the exit that produced it. */
|
|
15
|
+
readonly text: string
|
|
16
|
+
/** The exits that text accounts for, taken from the record. */
|
|
17
|
+
readonly exits: readonly BackgroundJob[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Background jobs the model SAID it is waiting on.
|
|
22
|
+
*
|
|
23
|
+
* `CompletionInbox` gives a run a bounded, zero-token wait for a delegated
|
|
24
|
+
* task nobody is blocked on, and `holdForOutstandingWork` spends it: the
|
|
25
|
+
* model stops calling tools, the loop races the inbox against operator input,
|
|
26
|
+
* and the run only settles once the result is in the transcript or the grace
|
|
27
|
+
* is gone. Background shell jobs had none of it — so a model that started a
|
|
28
|
+
* job and then had nothing left to do improvised, and the recorded run
|
|
29
|
+
* (research/resident/results/2026-09-14-exploration-policy-terra-tui.json)
|
|
30
|
+
* shows what that costs: six `job read` polls, three `job list` polls and a
|
|
31
|
+
* `sleep 30`, each one a full context resend.
|
|
32
|
+
*
|
|
33
|
+
* This is the same three-method shape the hold already races —
|
|
34
|
+
* {@link hasPendingWork}, {@link waitForArrival}, {@link drain} — over the
|
|
35
|
+
* job registry instead of the task gateway.
|
|
36
|
+
*
|
|
37
|
+
* **Intent is stated, never inferred.** A job is only outstanding here once
|
|
38
|
+
* `wait_for_job` named it (see {@link expect}); job existence means nothing.
|
|
39
|
+
* That is the same distinction `CompletionInbox.expect` draws for a
|
|
40
|
+
* background task, and it is what keeps a dev server or a file watcher —
|
|
41
|
+
* started precisely so it would keep running — from holding every run of the
|
|
42
|
+
* session open for its grace period. The model that wants to wait says so.
|
|
43
|
+
*/
|
|
44
|
+
export class AwaitedJobs {
|
|
45
|
+
/** Awaited and still running. Nothing else can hold a run open. */
|
|
46
|
+
private readonly outstanding = new Set<string>()
|
|
47
|
+
/** Exits since the last {@link drain}. */
|
|
48
|
+
private exits: BackgroundJob[] = []
|
|
49
|
+
private readonly arrivals = new Set<() => void>()
|
|
50
|
+
private detach?: () => void
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
private readonly source: AwaitedJobSource,
|
|
54
|
+
/** Whose jobs these are — the run or the session the jobs are bound to. */
|
|
55
|
+
private readonly owner: string,
|
|
56
|
+
/**
|
|
57
|
+
* Whether the exit notice this run queues for the model is still
|
|
58
|
+
* unread. A second opinion on {@link noticesDelivered}, and it can
|
|
59
|
+
* only ever narrow what counts as pending.
|
|
60
|
+
*
|
|
61
|
+
* An exit is work only until the model has seen it, and this adapter is
|
|
62
|
+
* not what shows it: the text rides out on the next tool result
|
|
63
|
+
* (`attachNotice`), which is what normally happens, long before any
|
|
64
|
+
* hold opens. {@link noticesDelivered} is how that delivery says so,
|
|
65
|
+
* and it is the per-job record — the entry goes, so nothing can count
|
|
66
|
+
* it again.
|
|
67
|
+
*
|
|
68
|
+
* This stays because the two answer different questions. That one is
|
|
69
|
+
* reported BY a delivery site, so it is only as good as the sites that
|
|
70
|
+
* remember to call it; this reads the channel itself, and a channel
|
|
71
|
+
* with nothing queued cannot have news in it whatever the record says.
|
|
72
|
+
* It is also safe in one direction only: it can suppress a hold, never
|
|
73
|
+
* open one, so a stale `true` here is not a turn the model gets to
|
|
74
|
+
* spend — which is the failure this pair exists to prevent.
|
|
75
|
+
*
|
|
76
|
+
* `CompletionInbox.claim` draws the same line on the task side. It can
|
|
77
|
+
* name the task, because the call that delivers a completion knows
|
|
78
|
+
* which one it delivered; a notice is text by the time it reaches the
|
|
79
|
+
* tool result, so this asks the channel instead.
|
|
80
|
+
*
|
|
81
|
+
* Absent means nothing else consumes the notices, so a recorded exit is
|
|
82
|
+
* unread by definition.
|
|
83
|
+
*/
|
|
84
|
+
private readonly unreadNotice?: () => boolean,
|
|
85
|
+
) {}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Start listening.
|
|
89
|
+
*
|
|
90
|
+
* Returns the detach, and attaching twice is a no-op rather than a second
|
|
91
|
+
* subscription — the same rule `CompletionInbox.attach` follows, for the
|
|
92
|
+
* same reason: two subscriptions would record each exit twice.
|
|
93
|
+
*/
|
|
94
|
+
attach(): () => void {
|
|
95
|
+
if (this.detach) return this.detach
|
|
96
|
+
this.detach = this.source.onExit((job) => {
|
|
97
|
+
// `onExit` is a broadcast over a registry a host may share between
|
|
98
|
+
// runs, and the filter is the same one the exit-notice channel
|
|
99
|
+
// applies in `runtime/query/index.ts`.
|
|
100
|
+
if (job.owner !== this.owner) return
|
|
101
|
+
// `outstanding` is the gate, so an id nobody awaited is ignored and
|
|
102
|
+
// a second announcement for the same job cannot queue twice.
|
|
103
|
+
if (!this.outstanding.delete(job.id)) return
|
|
104
|
+
this.exits.push(job)
|
|
105
|
+
for (const wake of [...this.arrivals]) wake()
|
|
106
|
+
})
|
|
107
|
+
return this.detach
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Say that the model is waiting on this job's exit.
|
|
112
|
+
*
|
|
113
|
+
* Called by `wait_for_job`, and by nothing else: the wait is the whole
|
|
114
|
+
* signal. A job that has ALREADY stopped is not recorded, because the
|
|
115
|
+
* call that marks it is the same call that returns its output — holding
|
|
116
|
+
* the run open afterwards would buy a turn to read a result the model has
|
|
117
|
+
* just read. `CompletionInbox.expect` skips an already-claimed task for
|
|
118
|
+
* the same reason.
|
|
119
|
+
*
|
|
120
|
+
* An id the registry does not know is ignored rather than raised: the
|
|
121
|
+
* caller is about to fail on it anyway, and a marking call is not the
|
|
122
|
+
* place that decides what an unknown job means.
|
|
123
|
+
*/
|
|
124
|
+
expect(id: string): void {
|
|
125
|
+
let job: BackgroundJob
|
|
126
|
+
try {
|
|
127
|
+
job = this.source.get(id)
|
|
128
|
+
} catch {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
if (job.owner !== this.owner) return
|
|
132
|
+
if (job.status !== 'running') return
|
|
133
|
+
this.outstanding.add(id)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Recorded exits the model has not been shown yet.
|
|
138
|
+
*
|
|
139
|
+
* Both halves of the same question: {@link noticesDelivered} drops the
|
|
140
|
+
* entry when its notice goes out, and the `unreadNotice` constructor
|
|
141
|
+
* argument asks the channel whether anything is still queued. A queued
|
|
142
|
+
* exit whose notice has already been delivered is history, not pending
|
|
143
|
+
* work, and an exit nobody awaited is not this adapter's news to carry.
|
|
144
|
+
*/
|
|
145
|
+
private get unreadExits(): boolean {
|
|
146
|
+
if (this.exits.length === 0) return false
|
|
147
|
+
return this.unreadNotice?.() ?? true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Whether an awaited job is still running, or one has exited unread. */
|
|
151
|
+
get hasPendingWork(): boolean {
|
|
152
|
+
return this.unreadExits || this.outstanding.size > 0
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Wait for the next awaited job's exit, deadline or abort, whichever comes
|
|
157
|
+
* first. Aborting releases only this waiter; the jobs are untouched.
|
|
158
|
+
*
|
|
159
|
+
* Bounded by the caller, exactly as `CompletionInbox.waitForArrival` is: a
|
|
160
|
+
* job that never exits — a dev server someone did await — must not keep a
|
|
161
|
+
* run open, and only the run's own budget knows how long is long enough.
|
|
162
|
+
*/
|
|
163
|
+
waitForArrival(timeoutMs: number, signal?: AbortSignal): Promise<void> {
|
|
164
|
+
if (signal?.aborted) return Promise.resolve()
|
|
165
|
+
if (this.unreadExits) return Promise.resolve()
|
|
166
|
+
if (this.outstanding.size === 0) return Promise.resolve()
|
|
167
|
+
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
const finish = (): void => {
|
|
170
|
+
clearTimeout(timer)
|
|
171
|
+
this.arrivals.delete(finish)
|
|
172
|
+
signal?.removeEventListener('abort', finish)
|
|
173
|
+
resolve()
|
|
174
|
+
}
|
|
175
|
+
const timer = setTimeout(finish, timeoutMs)
|
|
176
|
+
// `unref` where the runtime has it, so a pending wait never keeps a
|
|
177
|
+
// process alive past the job it was waiting for.
|
|
178
|
+
;(timer as { unref?: () => void }).unref?.()
|
|
179
|
+
|
|
180
|
+
this.arrivals.add(finish)
|
|
181
|
+
signal?.addEventListener('abort', finish, { once: true })
|
|
182
|
+
if (signal?.aborted) finish()
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Say that the queued exit notices have been put in front of the model.
|
|
188
|
+
*
|
|
189
|
+
* Called by the tool-result delivery (`attachNotice`), which is where an
|
|
190
|
+
* exit normally reaches the model; {@link takeDelivery} does the same for
|
|
191
|
+
* the hold's own path by taking the entries as it delivers them.
|
|
192
|
+
*
|
|
193
|
+
* Every recorded exit goes, and that is per-job rather than a blunt
|
|
194
|
+
* clear: the channel hands over ALL its queued text at once, and an exit
|
|
195
|
+
* is recorded here in the same synchronous announcement that queues its
|
|
196
|
+
* notice, so the text just delivered is exactly the notices of the exits
|
|
197
|
+
* in hand. An exit recorded after this call keeps its entry, because its
|
|
198
|
+
* notice was queued after that text was taken.
|
|
199
|
+
*
|
|
200
|
+
* Without it the entry outlived the delivery, and the channel-level gate
|
|
201
|
+
* above was the only thing standing between a read exit and a model turn
|
|
202
|
+
* — a gate that reopens the moment ANY later job, awaited or not, queues
|
|
203
|
+
* a notice of its own.
|
|
204
|
+
*/
|
|
205
|
+
noticesDelivered(): void {
|
|
206
|
+
this.exits = []
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Take the recorded exits together with the text that delivers them, or
|
|
211
|
+
* take neither.
|
|
212
|
+
*
|
|
213
|
+
* The pairing is the whole method. Draining the exits first and then
|
|
214
|
+
* asking for the notice loses them on the branch that finds none: the
|
|
215
|
+
* records are gone, no text was written, and the exit the run held itself
|
|
216
|
+
* open for is delivered by nobody. So {@link drain} is not called until
|
|
217
|
+
* the delivery is certain, and an exit this could not deliver stays in
|
|
218
|
+
* hand for the next one.
|
|
219
|
+
*/
|
|
220
|
+
takeDelivery(notice: () => string | undefined): DeliverableJobExits | undefined {
|
|
221
|
+
if (this.exits.length === 0) return undefined
|
|
222
|
+
const text = notice()
|
|
223
|
+
if (text === undefined) return undefined
|
|
224
|
+
return { text, exits: this.drain() }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Take every exit recorded since the last call, leaving none behind.
|
|
229
|
+
*
|
|
230
|
+
* Draining rather than peeking, for the reason `CompletionInbox.drain`
|
|
231
|
+
* gives: an exit that stays queued after being delivered is a duplicate
|
|
232
|
+
* waiting to happen. Callers that are delivering want
|
|
233
|
+
* {@link takeDelivery}, which will not take them without the notice that
|
|
234
|
+
* accounts for them.
|
|
235
|
+
*/
|
|
236
|
+
drain(): readonly BackgroundJob[] {
|
|
237
|
+
if (this.exits.length === 0) return []
|
|
238
|
+
const exits = this.exits
|
|
239
|
+
this.exits = []
|
|
240
|
+
return exits
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Awaited jobs still running.
|
|
245
|
+
*
|
|
246
|
+
* Read when a run ends, so it can say which wait it walked away from.
|
|
247
|
+
* Nothing here is stopped by being read — the ids are a statement, and a
|
|
248
|
+
* job's lifetime belongs to whoever owns it.
|
|
249
|
+
*/
|
|
250
|
+
get outstandingJobIds(): readonly string[] {
|
|
251
|
+
return [...this.outstanding]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Stop listening. Safe to call more than once.
|
|
256
|
+
*
|
|
257
|
+
* A run that ends without this leaves its listener on a registry the host
|
|
258
|
+
* reuses across runs — the leak `CompletionInbox.close` exists to prevent,
|
|
259
|
+
* on the other subsystem.
|
|
260
|
+
*/
|
|
261
|
+
close(): void {
|
|
262
|
+
this.detach?.()
|
|
263
|
+
this.detach = undefined
|
|
264
|
+
this.outstanding.clear()
|
|
265
|
+
this.exits = []
|
|
266
|
+
// Release anyone still waiting. A closed adapter would otherwise hold
|
|
267
|
+
// them to their own deadline for an exit that can no longer reach them.
|
|
268
|
+
for (const wake of [...this.arrivals]) wake()
|
|
269
|
+
this.arrivals.clear()
|
|
270
|
+
}
|
|
271
|
+
}
|