@namzu/sdk 5.0.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.
- package/CHANGELOG.md +463 -0
- package/README.md +2 -2
- 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 +109 -0
- package/dist/provider/__tests__/strict-schema.test.js.map +1 -0
- package/dist/provider/__tests__/typed-error-classification.test.d.ts +2 -0
- package/dist/provider/__tests__/typed-error-classification.test.d.ts.map +1 -0
- package/dist/provider/__tests__/typed-error-classification.test.js +123 -0
- package/dist/provider/__tests__/typed-error-classification.test.js.map +1 -0
- 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/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +23 -7
- package/dist/provider/retry.js.map +1 -1
- 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 +112 -0
- package/dist/provider/strict-schema.js.map +1 -0
- package/dist/public-runtime.d.ts +6 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +21 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +1 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +4 -0
- package/dist/public-tools.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__/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/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/runtime/query/__tests__/stream-recovery.test.js +5 -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 +5 -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/errors.d.ts +0 -17
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +60 -0
- package/dist/types/provider/errors.js.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 +133 -0
- package/src/provider/__tests__/typed-error-classification.test.ts +143 -0
- package/src/provider/model-version.ts +98 -0
- package/src/provider/retry.ts +23 -7
- package/src/provider/strict-schema.ts +125 -0
- package/src/public-runtime.ts +31 -0
- package/src/public-tools.ts +4 -0
- package/src/public-types.ts +8 -0
- package/src/registry/tool/__tests__/strict-registration.test.ts +115 -0
- package/src/registry/tool/execute.ts +16 -0
- package/src/runtime/query/__tests__/stream-recovery.test.ts +5 -0
- package/src/runtime/query/result.ts +5 -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/errors.ts +71 -0
- 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,143 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { classifyProviderError } from '../../types/provider/errors.js'
|
|
4
|
+
import type { LLMProvider, StreamChunk } from '../../types/provider/index.js'
|
|
5
|
+
import { ProviderRequestError } from '../errors.js'
|
|
6
|
+
import { withProviderRetry } from '../retry.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A driver that classified its own failure was coming out WORSE than one that
|
|
10
|
+
* did not, in two independent places, and both shipped.
|
|
11
|
+
*
|
|
12
|
+
* `classifyProviderError` never read `kind`. A `ProviderRequestError` fell
|
|
13
|
+
* through to the status heuristics, where a carefully-diagnosed
|
|
14
|
+
* `context_overflow` carrying a 400 became `invalid_request` — so the run
|
|
15
|
+
* loop's overflow branch, which tests for `context_length_exceeded`, could
|
|
16
|
+
* never fire for a first-party driver, and compaction relief was unreachable
|
|
17
|
+
* for exactly the drivers that had diagnosed the problem correctly.
|
|
18
|
+
*
|
|
19
|
+
* And `withProviderRetry` rethrew any such error before the retry loop. Its
|
|
20
|
+
* comment justified preserving the classification, which is right; the code
|
|
21
|
+
* also skipped retrying, which is a different decision that nobody made.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function providerThatFails(err: unknown): { provider: LLMProvider; calls: () => number } {
|
|
25
|
+
let calls = 0
|
|
26
|
+
const provider = {
|
|
27
|
+
id: 'test',
|
|
28
|
+
name: 'Test',
|
|
29
|
+
async *chatStream(): AsyncIterable<StreamChunk> {
|
|
30
|
+
calls++
|
|
31
|
+
throw err
|
|
32
|
+
// biome-ignore lint/correctness/useYield: it fails before producing anything
|
|
33
|
+
},
|
|
34
|
+
} as unknown as LLMProvider
|
|
35
|
+
return { provider, calls: () => calls }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const typed = (kind: string, status: number) =>
|
|
39
|
+
new ProviderRequestError({ kind, providerId: 'test', status, message: `a ${kind}` } as never)
|
|
40
|
+
|
|
41
|
+
describe('a driver that classified its own failure is believed', () => {
|
|
42
|
+
const cases: readonly [string, number, string, boolean][] = [
|
|
43
|
+
['throttle', 429, 'rate_limit', true],
|
|
44
|
+
['server', 500, 'server_error', true],
|
|
45
|
+
['network', 0, 'network', true],
|
|
46
|
+
['auth', 401, 'auth', false],
|
|
47
|
+
['bad_request', 400, 'invalid_request', false],
|
|
48
|
+
// The one that mattered most: a 400 whose kind says the prompt was too
|
|
49
|
+
// long is not a bad request, and the difference decides whether the
|
|
50
|
+
// kernel reaches for compaction.
|
|
51
|
+
['context_overflow', 400, 'context_length_exceeded', false],
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
for (const [kind, status, code, retryable] of cases) {
|
|
55
|
+
it(`maps kind "${kind}" to ${code}`, () => {
|
|
56
|
+
const classified = classifyProviderError(typed(kind, status), 'test')
|
|
57
|
+
expect(classified.code).toBe(code)
|
|
58
|
+
expect(classified.retryable).toBe(retryable)
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
it('keeps the driver-supplied status and provider id', () => {
|
|
63
|
+
const classified = classifyProviderError(typed('throttle', 429), 'other')
|
|
64
|
+
expect(classified.status).toBe(429)
|
|
65
|
+
expect(classified.providerId).toBe('test')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('a classified failure still goes through the retry loop', () => {
|
|
70
|
+
it('retries a typed throttle', async () => {
|
|
71
|
+
// This is the regression: a first-party driver reporting a 429 as
|
|
72
|
+
// `kind: 'throttle'` used to get exactly one attempt, while the same
|
|
73
|
+
// failure from a driver that classified nothing got the full backoff.
|
|
74
|
+
const { provider, calls } = providerThatFails(typed('throttle', 429))
|
|
75
|
+
const wrapped = withProviderRetry(provider, {
|
|
76
|
+
config: { maxRetries: 2 },
|
|
77
|
+
sleepFn: async () => {},
|
|
78
|
+
random: () => 0,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
await expect(async () => {
|
|
82
|
+
for await (const _ of wrapped.chatStream({} as never)) {
|
|
83
|
+
// drain
|
|
84
|
+
}
|
|
85
|
+
}).rejects.toThrow()
|
|
86
|
+
|
|
87
|
+
expect(calls()).toBe(3)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('does not retry a typed auth failure', async () => {
|
|
91
|
+
const { provider, calls } = providerThatFails(typed('auth', 401))
|
|
92
|
+
const wrapped = withProviderRetry(provider, {
|
|
93
|
+
config: { maxRetries: 2 },
|
|
94
|
+
sleepFn: async () => {},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
await expect(async () => {
|
|
98
|
+
for await (const _ of wrapped.chatStream({} as never)) {
|
|
99
|
+
// drain
|
|
100
|
+
}
|
|
101
|
+
}).rejects.toThrow()
|
|
102
|
+
|
|
103
|
+
expect(calls()).toBe(1)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('does not retry a typed context overflow', async () => {
|
|
107
|
+
// Correctly non-retryable — an identical prompt overflows identically.
|
|
108
|
+
// The remedy is compaction, which the run loop reaches for once the
|
|
109
|
+
// code is `context_length_exceeded`.
|
|
110
|
+
const { provider, calls } = providerThatFails(typed('context_overflow', 400))
|
|
111
|
+
const wrapped = withProviderRetry(provider, {
|
|
112
|
+
config: { maxRetries: 2 },
|
|
113
|
+
sleepFn: async () => {},
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
await expect(async () => {
|
|
117
|
+
for await (const _ of wrapped.chatStream({} as never)) {
|
|
118
|
+
// drain
|
|
119
|
+
}
|
|
120
|
+
// The ORIGINAL escapes, so the boundary still sees the driver's
|
|
121
|
+
// own kind rather than a wrapper's code.
|
|
122
|
+
}).rejects.toMatchObject({ kind: 'context_overflow', status: 400 })
|
|
123
|
+
|
|
124
|
+
expect(calls()).toBe(1)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('leaves an abort alone', async () => {
|
|
128
|
+
const abort = Object.assign(new Error('aborted'), { name: 'AbortError' })
|
|
129
|
+
const { provider, calls } = providerThatFails(abort)
|
|
130
|
+
const wrapped = withProviderRetry(provider, {
|
|
131
|
+
config: { maxRetries: 2 },
|
|
132
|
+
sleepFn: async () => {},
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
await expect(async () => {
|
|
136
|
+
for await (const _ of wrapped.chatStream({} as never)) {
|
|
137
|
+
// drain
|
|
138
|
+
}
|
|
139
|
+
}).rejects.toThrow(/aborted/)
|
|
140
|
+
|
|
141
|
+
expect(calls()).toBe(1)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
@@ -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
|
+
}
|
package/src/provider/retry.ts
CHANGED
|
@@ -111,12 +111,19 @@ export function withProviderRetry(
|
|
|
111
111
|
return
|
|
112
112
|
} catch (err) {
|
|
113
113
|
if (isAbortError(err) || params.signal?.aborted) throw err
|
|
114
|
-
// A driver that already classified its own failure
|
|
115
|
-
//
|
|
116
|
-
// first
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
// A driver that already classified its own failure keeps that
|
|
115
|
+
// classification — `classifyProviderError` reads its `kind`
|
|
116
|
+
// first and does not re-guess.
|
|
117
|
+
//
|
|
118
|
+
// This used to rethrow such an error outright. The stated reason
|
|
119
|
+
// was sound and the code did more than it said: preserving a
|
|
120
|
+
// first-hand classification is one thing, and skipping the retry
|
|
121
|
+
// loop is another. A first-party driver that correctly reported
|
|
122
|
+
// a 429 as `kind: 'throttle'` got ZERO attempts, while the same
|
|
123
|
+
// failure from a driver that classified nothing got the full
|
|
124
|
+
// backoff — so diagnosing your own error was punished. Whether
|
|
125
|
+
// to retry is now decided the same way for both, by the
|
|
126
|
+
// classification's own `retryable`.
|
|
120
127
|
const classified = classifyProviderError(err, provider.id)
|
|
121
128
|
const exhausted = attempt >= config.maxRetries
|
|
122
129
|
|
|
@@ -133,7 +140,16 @@ export function withProviderRetry(
|
|
|
133
140
|
? 'retries exhausted'
|
|
134
141
|
: 'not retryable',
|
|
135
142
|
})
|
|
136
|
-
|
|
143
|
+
// The ORIGINAL escapes when the driver classified it. Two
|
|
144
|
+
// different consumers want two different things and both are
|
|
145
|
+
// right: this loop needs a retryable verdict, which the
|
|
146
|
+
// classification supplies, and the run boundary reports
|
|
147
|
+
// `lastProviderError` as the driver's own `{kind, status,
|
|
148
|
+
// retryAfterMs}`, which only survives if the error itself
|
|
149
|
+
// does. Wrapping here would have kept the retry fix and lost
|
|
150
|
+
// the vendor's `kind` at the boundary — the existing
|
|
151
|
+
// stream-recovery test caught exactly that.
|
|
152
|
+
throw isProviderRequestError(err) ? err : classified
|
|
137
153
|
}
|
|
138
154
|
|
|
139
155
|
const serverDirected = classified.retryAfterMs
|
|
@@ -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
|
+
}
|
package/src/public-runtime.ts
CHANGED
|
@@ -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
|
|
package/src/public-tools.ts
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
// ─── Tool definition primitive ───────────────────────────────────────────
|
|
13
13
|
|
|
14
14
|
export { defineTool } from './tools/defineTool.js'
|
|
15
|
+
// Containment, for a host or sibling package that resolves caller-supplied
|
|
16
|
+
// paths against a root. Both were internal while three call sites outside
|
|
17
|
+
// this file needed them.
|
|
18
|
+
export { isWithin, resolveWithin, resolveWithinReal } from './tools/paths.js'
|
|
15
19
|
// A host that surfaces its own untrusted content to a model needs the same
|
|
16
20
|
// framing the kernel applies to connector prompts and delegated results.
|
|
17
21
|
export { neutralizeEnvelopeDelimiter, wrapUntrusted } from './tools/untrusted-envelope.js'
|
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'
|