@namzu/sdk 1.1.0 → 1.3.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 +98 -0
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +2 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +4 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/{runtime/query → constants}/continuation.d.ts +5 -0
- package/dist/constants/continuation.d.ts.map +1 -0
- package/dist/{runtime/query → constants}/continuation.js +5 -0
- package/dist/constants/continuation.js.map +1 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +1 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +11 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +30 -1
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/provider/__tests__/registry.test.d.ts +5 -0
- package/dist/provider/__tests__/registry.test.d.ts.map +1 -1
- package/dist/provider/__tests__/registry.test.js +186 -1
- package/dist/provider/__tests__/registry.test.js.map +1 -1
- package/dist/provider/capabilities.d.ts +27 -0
- package/dist/provider/capabilities.d.ts.map +1 -0
- package/dist/provider/capabilities.js +29 -0
- package/dist/provider/capabilities.js.map +1 -0
- package/dist/provider/index.d.ts +3 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +2 -1
- package/dist/provider/index.js.map +1 -1
- package/dist/provider/registry.d.ts +81 -1
- package/dist/provider/registry.d.ts.map +1 -1
- package/dist/provider/registry.js +173 -7
- package/dist/provider/registry.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +8 -0
- package/dist/run/reporter.js.map +1 -1
- package/dist/runtime/query/__tests__/capability-negotiation.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.js +184 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -0
- package/dist/runtime/query/__tests__/checkpoint-store.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/checkpoint-store.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/checkpoint-store.test.js +259 -0
- package/dist/runtime/query/__tests__/checkpoint-store.test.js.map +1 -0
- package/dist/runtime/query/__tests__/checkpoint.test.js +9 -3
- package/dist/runtime/query/__tests__/checkpoint.test.js.map +1 -1
- package/dist/runtime/query/__tests__/orphan-task-turn-end.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/orphan-task-turn-end.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/orphan-task-turn-end.test.js +110 -0
- package/dist/runtime/query/__tests__/orphan-task-turn-end.test.js.map +1 -0
- package/dist/runtime/query/checkpoint.d.ts +16 -2
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +21 -7
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/context.d.ts +7 -0
- package/dist/runtime/query/context.d.ts.map +1 -1
- package/dist/runtime/query/context.js +1 -0
- package/dist/runtime/query/context.js.map +1 -1
- package/dist/runtime/query/index.d.ts +20 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +75 -6
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +3 -57
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +246 -735
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/checkpoint.js +22 -0
- package/dist/runtime/query/iteration/phases/checkpoint.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +1 -2
- 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/stream-turn.d.ts +35 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -0
- package/dist/runtime/query/iteration/stream-turn.js +341 -0
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -0
- package/dist/runtime/query/replay/list.d.ts +17 -1
- package/dist/runtime/query/replay/list.d.ts.map +1 -1
- package/dist/runtime/query/replay/list.js +21 -4
- package/dist/runtime/query/replay/list.js.map +1 -1
- package/dist/runtime/query/replay/prepare.d.ts +10 -0
- package/dist/runtime/query/replay/prepare.d.ts.map +1 -1
- package/dist/runtime/query/replay/prepare.js +20 -4
- package/dist/runtime/query/replay/prepare.js.map +1 -1
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/run/checkpoint-disk.d.ts +28 -0
- package/dist/store/run/checkpoint-disk.d.ts.map +1 -0
- package/dist/store/run/checkpoint-disk.js +55 -0
- package/dist/store/run/checkpoint-disk.js.map +1 -0
- package/dist/types/provider/config.d.ts +51 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/provider/index.d.ts +1 -1
- package/dist/types/provider/index.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +10 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/run/checkpoint-store.d.ts +64 -0
- package/dist/types/run/checkpoint-store.d.ts.map +1 -0
- package/dist/types/run/checkpoint-store.js +12 -0
- package/dist/types/run/checkpoint-store.js.map +1 -0
- package/dist/types/run/config.d.ts +27 -0
- package/dist/types/run/config.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +6 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/run/index.d.ts +1 -0
- package/dist/types/run/index.d.ts.map +1 -1
- package/dist/types/run/index.js +1 -0
- package/dist/types/run/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bridge/a2a/mapper.ts +3 -0
- package/src/bridge/sse/mapper.ts +5 -0
- package/src/{runtime/query → constants}/continuation.ts +5 -0
- package/src/constants/index.ts +1 -0
- package/src/manager/run/persistence.ts +34 -1
- package/src/provider/__tests__/registry.test.ts +266 -1
- package/src/provider/capabilities.ts +47 -0
- package/src/provider/index.ts +12 -1
- package/src/provider/registry.ts +202 -7
- package/src/public-runtime.ts +6 -0
- package/src/run/reporter.ts +9 -0
- package/src/runtime/query/__tests__/capability-negotiation.test.ts +248 -0
- package/src/runtime/query/__tests__/checkpoint-store.test.ts +322 -0
- package/src/runtime/query/__tests__/checkpoint.test.ts +15 -6
- package/src/runtime/query/__tests__/orphan-task-turn-end.test.ts +127 -0
- package/src/runtime/query/checkpoint.ts +23 -9
- package/src/runtime/query/context.ts +9 -0
- package/src/runtime/query/index.ts +115 -28
- package/src/runtime/query/iteration/index.ts +298 -905
- package/src/runtime/query/iteration/phases/checkpoint.ts +25 -0
- package/src/runtime/query/iteration/phases/context.ts +1 -3
- package/src/runtime/query/iteration/stream-turn.ts +396 -0
- package/src/runtime/query/replay/list.ts +38 -4
- package/src/runtime/query/replay/prepare.ts +37 -4
- package/src/store/index.ts +1 -0
- package/src/store/run/checkpoint-disk.ts +67 -0
- package/src/types/provider/config.ts +54 -0
- package/src/types/provider/index.ts +3 -0
- package/src/types/provider/interface.ts +11 -0
- package/src/types/run/checkpoint-store.ts +72 -0
- package/src/types/run/config.ts +30 -0
- package/src/types/run/events.ts +13 -0
- package/src/types/run/index.ts +1 -0
- package/dist/runtime/query/continuation.d.ts.map +0 -1
- package/dist/runtime/query/continuation.js.map +0 -1
package/src/provider/registry.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
LLMProvider,
|
|
3
3
|
LLMProviderConstructor,
|
|
4
|
+
LazyProviderLoader,
|
|
5
|
+
LazyProviderModule,
|
|
4
6
|
ProviderCapabilities,
|
|
5
7
|
ProviderConfigRegistry,
|
|
6
8
|
ProviderFactoryConfig,
|
|
7
9
|
ProviderFactoryResult,
|
|
8
10
|
ProviderType,
|
|
11
|
+
RegisterLazyOptions,
|
|
9
12
|
RegisterOptions,
|
|
10
13
|
} from '../types/provider/index.js'
|
|
14
|
+
import { PERMISSIVE_PROVIDER_CAPABILITIES } from './capabilities.js'
|
|
11
15
|
|
|
12
16
|
export class UnknownProviderError extends Error {
|
|
13
17
|
readonly providerType: string
|
|
@@ -31,9 +35,99 @@ export class DuplicateProviderError extends Error {
|
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The loader passed to `ProviderRegistry.registerLazy()` rejected (or
|
|
40
|
+
* resolved to something without a `create(config)` function). Wraps the
|
|
41
|
+
* original failure as `cause`. The failed load is NOT cached — the next
|
|
42
|
+
* `createAsync()` for the type re-invokes the loader, so a transient
|
|
43
|
+
* failure (network hiccup during a dynamic import) does not permanently
|
|
44
|
+
* poison the type.
|
|
45
|
+
*/
|
|
46
|
+
export class LazyProviderLoadError extends Error {
|
|
47
|
+
readonly providerType: string
|
|
48
|
+
|
|
49
|
+
constructor(providerType: string, cause: unknown) {
|
|
50
|
+
const detail = cause instanceof Error ? cause.message : String(cause)
|
|
51
|
+
super(`Failed to load lazy provider "${providerType}": ${detail}`, { cause })
|
|
52
|
+
this.name = 'LazyProviderLoadError'
|
|
53
|
+
this.providerType = providerType
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A synchronous `create()`/`createProvider()` was called for a type
|
|
59
|
+
* registered via `registerLazy()`. Lazy types are only constructible
|
|
60
|
+
* through the async path — deterministically, even after the loader has
|
|
61
|
+
* resolved, so calling code never depends on load-order timing.
|
|
62
|
+
*/
|
|
63
|
+
export class LazyProviderSyncCreateError extends Error {
|
|
64
|
+
readonly providerType: string
|
|
65
|
+
|
|
66
|
+
constructor(providerType: string) {
|
|
67
|
+
super(
|
|
68
|
+
`Provider type "${providerType}" is registered lazily; synchronous create()/createProvider() cannot load it. Use ProviderRegistry.createAsync() or createProviderAsync().`,
|
|
69
|
+
)
|
|
70
|
+
this.name = 'LazyProviderSyncCreateError'
|
|
71
|
+
this.providerType = providerType
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface LazyProviderEntry {
|
|
76
|
+
loader: LazyProviderLoader<unknown>
|
|
77
|
+
/** In-flight load shared by concurrent first-creates (dedupe). */
|
|
78
|
+
loading?: Promise<LazyProviderModule<unknown>>
|
|
79
|
+
/** Cached module — set only on SUCCESS, so failures retry. */
|
|
80
|
+
module?: LazyProviderModule<unknown>
|
|
81
|
+
}
|
|
82
|
+
|
|
34
83
|
// Module-private state. Only the exported functions below can read/mutate.
|
|
35
84
|
const providers = new Map<string, LLMProviderConstructor<unknown>>()
|
|
36
85
|
const capabilities = new Map<string, ProviderCapabilities>()
|
|
86
|
+
const lazyProviders = new Map<string, LazyProviderEntry>()
|
|
87
|
+
|
|
88
|
+
async function loadLazyModule(
|
|
89
|
+
type: string,
|
|
90
|
+
entry: LazyProviderEntry,
|
|
91
|
+
): Promise<LazyProviderModule<unknown>> {
|
|
92
|
+
if (entry.module) {
|
|
93
|
+
return entry.module
|
|
94
|
+
}
|
|
95
|
+
let inflight = entry.loading
|
|
96
|
+
if (!inflight) {
|
|
97
|
+
// Promise.resolve().then(...) also catches loaders that throw synchronously.
|
|
98
|
+
inflight = Promise.resolve()
|
|
99
|
+
.then(() => entry.loader())
|
|
100
|
+
.then((mod) => {
|
|
101
|
+
if (typeof mod?.create !== 'function') {
|
|
102
|
+
throw new TypeError(
|
|
103
|
+
'loader resolved to a value without a create(config) function — map your dynamic import to { create: (config) => new Provider(config) }',
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
return mod
|
|
107
|
+
})
|
|
108
|
+
entry.loading = inflight
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const mod = await inflight
|
|
112
|
+
entry.module = mod
|
|
113
|
+
if (entry.loading === inflight) {
|
|
114
|
+
entry.loading = undefined
|
|
115
|
+
}
|
|
116
|
+
// The loaded module's declaration is authoritative at the type level:
|
|
117
|
+
// it replaces any registration-time hint for future getCapabilities().
|
|
118
|
+
if (mod.capabilities) {
|
|
119
|
+
capabilities.set(type, mod.capabilities)
|
|
120
|
+
}
|
|
121
|
+
return mod
|
|
122
|
+
} catch (cause) {
|
|
123
|
+
// Clear only OUR in-flight promise; a retry started by another caller
|
|
124
|
+
// after an earlier failure must not be dropped.
|
|
125
|
+
if (entry.loading === inflight) {
|
|
126
|
+
entry.loading = undefined
|
|
127
|
+
}
|
|
128
|
+
throw new LazyProviderLoadError(type, cause)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
37
131
|
|
|
38
132
|
/**
|
|
39
133
|
* Central registry for LLM providers.
|
|
@@ -56,6 +150,23 @@ const capabilities = new Map<string, ProviderCapabilities>()
|
|
|
56
150
|
* region: 'us-east-1',
|
|
57
151
|
* })
|
|
58
152
|
* ```
|
|
153
|
+
*
|
|
154
|
+
* Hosts that must not eagerly bundle every provider client register a
|
|
155
|
+
* LOADER instead and construct through the async path:
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* ProviderRegistry.registerLazy(
|
|
160
|
+
* 'anthropic',
|
|
161
|
+
* async () => {
|
|
162
|
+
* const m = await import('@namzu/anthropic')
|
|
163
|
+
* return { create: (c) => new m.AnthropicProvider(c), capabilities: m.ANTHROPIC_CAPABILITIES }
|
|
164
|
+
* },
|
|
165
|
+
* { capabilities: { supportsTools: true, supportsStreaming: true, supportsFunctionCalling: true } },
|
|
166
|
+
* )
|
|
167
|
+
*
|
|
168
|
+
* const { provider } = await ProviderRegistry.createAsync({ type: 'anthropic', apiKey })
|
|
169
|
+
* ```
|
|
59
170
|
*/
|
|
60
171
|
export class ProviderRegistry {
|
|
61
172
|
static register<K extends ProviderType>(
|
|
@@ -64,46 +175,129 @@ export class ProviderRegistry {
|
|
|
64
175
|
caps: ProviderCapabilities,
|
|
65
176
|
options?: RegisterOptions,
|
|
66
177
|
): void {
|
|
67
|
-
if (providers.has(type) && !options?.replace) {
|
|
178
|
+
if ((providers.has(type) || lazyProviders.has(type)) && !options?.replace) {
|
|
68
179
|
throw new DuplicateProviderError(type)
|
|
69
180
|
}
|
|
181
|
+
lazyProviders.delete(type)
|
|
70
182
|
providers.set(type, ctor as LLMProviderConstructor<unknown>)
|
|
71
183
|
capabilities.set(type, caps)
|
|
72
184
|
}
|
|
73
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Register a provider type WITHOUT importing its implementation. The
|
|
188
|
+
* loader is not invoked here; the first `createAsync()` for the type
|
|
189
|
+
* awaits it, validates the resolved `{ create }` module, and caches it.
|
|
190
|
+
* Subsequent creates reuse the cached factory. Only SUCCESS is cached:
|
|
191
|
+
* a rejected load surfaces as `LazyProviderLoadError` and the next
|
|
192
|
+
* `createAsync()` retries the loader. Concurrent first-creates share a
|
|
193
|
+
* single in-flight load.
|
|
194
|
+
*
|
|
195
|
+
* Capability precedence (weakest first):
|
|
196
|
+
* 1. `options.capabilities` — pre-load HINT so `getCapabilities(type)`
|
|
197
|
+
* answers without loading (absent hint ⇒ permissive default, matching
|
|
198
|
+
* `resolveProviderCapabilities`'s treatment of undeclared providers).
|
|
199
|
+
* 2. the loaded module's `capabilities` — replaces the hint on load.
|
|
200
|
+
* 3. the constructed instance's own `LLMProvider.capabilities` — the
|
|
201
|
+
* query runtime negotiates against the INSTANCE
|
|
202
|
+
* (`resolveProviderCapabilities(provider)`), so if it differs from
|
|
203
|
+
* both of the above, the instance wins where it matters.
|
|
204
|
+
*
|
|
205
|
+
* Lazy types are deliberately NOT constructible via the sync
|
|
206
|
+
* `create()`/`createProvider()` (throws `LazyProviderSyncCreateError`),
|
|
207
|
+
* even after the loader has resolved — sync behavior must not depend on
|
|
208
|
+
* whether some earlier call happened to load the module.
|
|
209
|
+
*/
|
|
210
|
+
static registerLazy<K extends ProviderType>(
|
|
211
|
+
type: K,
|
|
212
|
+
loader: LazyProviderLoader<ProviderConfigRegistry[K]>,
|
|
213
|
+
options?: RegisterLazyOptions,
|
|
214
|
+
): void {
|
|
215
|
+
if ((providers.has(type) || lazyProviders.has(type)) && !options?.replace) {
|
|
216
|
+
throw new DuplicateProviderError(type)
|
|
217
|
+
}
|
|
218
|
+
providers.delete(type)
|
|
219
|
+
capabilities.delete(type)
|
|
220
|
+
lazyProviders.set(type, { loader: loader as LazyProviderLoader<unknown> })
|
|
221
|
+
if (options?.capabilities) {
|
|
222
|
+
capabilities.set(type, options.capabilities)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
74
226
|
static create(config: ProviderFactoryConfig): ProviderFactoryResult {
|
|
75
227
|
const provider = ProviderRegistry.createProvider(config)
|
|
76
228
|
const caps = ProviderRegistry.getCapabilities(config.type)
|
|
77
229
|
return { provider, capabilities: caps }
|
|
78
230
|
}
|
|
79
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Async twin of `create()`. Works for BOTH eager and lazy registrations,
|
|
234
|
+
* so hosts can use one code path; for lazy types it performs the
|
|
235
|
+
* load-on-first-use described on `registerLazy()`.
|
|
236
|
+
*/
|
|
237
|
+
static async createAsync(config: ProviderFactoryConfig): Promise<ProviderFactoryResult> {
|
|
238
|
+
const provider = await ProviderRegistry.createProviderAsync(config)
|
|
239
|
+
// Read capabilities AFTER construction so a lazily-loaded module's
|
|
240
|
+
// authoritative declaration (set during load) is what gets returned.
|
|
241
|
+
const caps = ProviderRegistry.getCapabilities(config.type)
|
|
242
|
+
return { provider, capabilities: caps }
|
|
243
|
+
}
|
|
244
|
+
|
|
80
245
|
static createProvider(config: ProviderFactoryConfig): LLMProvider {
|
|
81
246
|
const Ctor = providers.get(config.type)
|
|
82
247
|
if (!Ctor) {
|
|
248
|
+
if (lazyProviders.has(config.type)) {
|
|
249
|
+
throw new LazyProviderSyncCreateError(config.type)
|
|
250
|
+
}
|
|
83
251
|
throw new UnknownProviderError(config.type)
|
|
84
252
|
}
|
|
85
253
|
return new Ctor(config)
|
|
86
254
|
}
|
|
87
255
|
|
|
256
|
+
static async createProviderAsync(config: ProviderFactoryConfig): Promise<LLMProvider> {
|
|
257
|
+
const Ctor = providers.get(config.type)
|
|
258
|
+
if (Ctor) {
|
|
259
|
+
return new Ctor(config)
|
|
260
|
+
}
|
|
261
|
+
const entry = lazyProviders.get(config.type)
|
|
262
|
+
if (!entry) {
|
|
263
|
+
throw new UnknownProviderError(config.type)
|
|
264
|
+
}
|
|
265
|
+
const mod = await loadLazyModule(config.type, entry)
|
|
266
|
+
return mod.create(config)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Type-level capabilities. For a lazily-registered type this answers
|
|
271
|
+
* WITHOUT invoking the loader: the registration hint if one was given,
|
|
272
|
+
* otherwise the permissive default (assume everything — consistent with
|
|
273
|
+
* how `resolveProviderCapabilities` treats an undeclared provider). Once
|
|
274
|
+
* loaded, a module-shipped declaration replaces the hint. Note the query
|
|
275
|
+
* runtime negotiates against the constructed INSTANCE's own
|
|
276
|
+
* `capabilities`, which wins over anything stored here.
|
|
277
|
+
*/
|
|
88
278
|
static getCapabilities(type: string): ProviderCapabilities {
|
|
89
279
|
const caps = capabilities.get(type)
|
|
90
|
-
if (
|
|
91
|
-
|
|
280
|
+
if (caps) {
|
|
281
|
+
return caps
|
|
282
|
+
}
|
|
283
|
+
if (lazyProviders.has(type)) {
|
|
284
|
+
return PERMISSIVE_PROVIDER_CAPABILITIES
|
|
92
285
|
}
|
|
93
|
-
|
|
286
|
+
throw new UnknownProviderError(type)
|
|
94
287
|
}
|
|
95
288
|
|
|
96
289
|
static isSupported(type: string): type is ProviderType {
|
|
97
|
-
return providers.has(type)
|
|
290
|
+
return providers.has(type) || lazyProviders.has(type)
|
|
98
291
|
}
|
|
99
292
|
|
|
100
293
|
static unregister(type: ProviderType): boolean {
|
|
101
294
|
capabilities.delete(type)
|
|
102
|
-
|
|
295
|
+
const hadLazy = lazyProviders.delete(type)
|
|
296
|
+
return providers.delete(type) || hadLazy
|
|
103
297
|
}
|
|
104
298
|
|
|
105
299
|
static listTypes(): ProviderType[] {
|
|
106
|
-
return Array.from(providers.keys()) as ProviderType[]
|
|
300
|
+
return Array.from(new Set([...providers.keys(), ...lazyProviders.keys()])) as ProviderType[]
|
|
107
301
|
}
|
|
108
302
|
}
|
|
109
303
|
|
|
@@ -115,4 +309,5 @@ export class ProviderRegistry {
|
|
|
115
309
|
export function __resetProviderRegistryInternal(): void {
|
|
116
310
|
providers.clear()
|
|
117
311
|
capabilities.clear()
|
|
312
|
+
lazyProviders.clear()
|
|
118
313
|
}
|
package/src/public-runtime.ts
CHANGED
|
@@ -99,6 +99,7 @@ export {
|
|
|
99
99
|
|
|
100
100
|
export {
|
|
101
101
|
ActivityStore,
|
|
102
|
+
DiskCheckpointStore,
|
|
102
103
|
DiskMemoryStore,
|
|
103
104
|
DiskTaskStore,
|
|
104
105
|
InMemoryMemoryIndex,
|
|
@@ -144,10 +145,14 @@ export { LocalTaskGateway } from './gateway/local.js'
|
|
|
144
145
|
|
|
145
146
|
export {
|
|
146
147
|
DuplicateProviderError,
|
|
148
|
+
LazyProviderLoadError,
|
|
149
|
+
LazyProviderSyncCreateError,
|
|
147
150
|
MOCK_CAPABILITIES,
|
|
148
151
|
MockLLMProvider,
|
|
152
|
+
PERMISSIVE_PROVIDER_CAPABILITIES,
|
|
149
153
|
ProviderRegistry,
|
|
150
154
|
registerMock,
|
|
155
|
+
resolveProviderCapabilities,
|
|
151
156
|
UnknownProviderError,
|
|
152
157
|
} from './provider/index.js'
|
|
153
158
|
|
|
@@ -248,6 +253,7 @@ export {
|
|
|
248
253
|
|
|
249
254
|
export { wrapProviderWithProbes } from './provider/instrumentation.js'
|
|
250
255
|
export type { ProviderInstrumentationOptions } from './provider/instrumentation.js'
|
|
256
|
+
export type { ResolvedProviderCapabilities } from './provider/capabilities.js'
|
|
251
257
|
export { collect } from './provider/collect.js'
|
|
252
258
|
|
|
253
259
|
export { wrapVaultWithProbes } from './vault/instrumentation.js'
|
package/src/run/reporter.ts
CHANGED
|
@@ -225,6 +225,15 @@ export function createRunReporter(parentLogger?: Logger): RunReporter {
|
|
|
225
225
|
})
|
|
226
226
|
break
|
|
227
227
|
|
|
228
|
+
case 'capability_warning':
|
|
229
|
+
log.warn('Provider capability mismatch', {
|
|
230
|
+
runId: event.runId,
|
|
231
|
+
capability: event.capability,
|
|
232
|
+
providerId: event.providerId,
|
|
233
|
+
message: event.message,
|
|
234
|
+
})
|
|
235
|
+
break
|
|
236
|
+
|
|
228
237
|
default: {
|
|
229
238
|
const _exhaustive: never = event
|
|
230
239
|
throw new Error(`Unhandled run event type: ${(_exhaustive as RunEvent).type}`)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
PERMISSIVE_PROVIDER_CAPABILITIES,
|
|
9
|
+
resolveProviderCapabilities,
|
|
10
|
+
} from '../../../provider/capabilities.js'
|
|
11
|
+
import { ToolRegistry } from '../../../registry/tool/execute.js'
|
|
12
|
+
import type { SessionId, TenantId } from '../../../types/ids/index.js'
|
|
13
|
+
import { createUserMessage } from '../../../types/message/index.js'
|
|
14
|
+
import type {
|
|
15
|
+
ChatCompletionParams,
|
|
16
|
+
LLMProvider,
|
|
17
|
+
ProviderCapabilities,
|
|
18
|
+
StreamChunk,
|
|
19
|
+
} from '../../../types/provider/index.js'
|
|
20
|
+
import type { RunEvent } from '../../../types/run/index.js'
|
|
21
|
+
import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
|
|
22
|
+
import { drainQuery } from '../index.js'
|
|
23
|
+
|
|
24
|
+
const ZERO_USAGE = {
|
|
25
|
+
promptTokens: 0,
|
|
26
|
+
completionTokens: 0,
|
|
27
|
+
totalTokens: 0,
|
|
28
|
+
cachedTokens: 0,
|
|
29
|
+
cacheWriteTokens: 0,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class CapturingProvider implements LLMProvider {
|
|
33
|
+
readonly id = 'capturing'
|
|
34
|
+
readonly name = 'Capturing Provider'
|
|
35
|
+
readonly capabilities?: ProviderCapabilities
|
|
36
|
+
lastParams?: ChatCompletionParams
|
|
37
|
+
|
|
38
|
+
constructor(capabilities?: ProviderCapabilities) {
|
|
39
|
+
this.capabilities = capabilities
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
|
|
43
|
+
this.lastParams = params
|
|
44
|
+
yield { id: 'msg_1', delta: { content: 'done' } }
|
|
45
|
+
yield { id: 'msg_1', delta: {}, finishReason: 'stop', usage: ZERO_USAGE }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const NO_TOOLS_CAPABILITIES: ProviderCapabilities = {
|
|
50
|
+
supportsTools: false,
|
|
51
|
+
supportsStreaming: true,
|
|
52
|
+
supportsFunctionCalling: false,
|
|
53
|
+
supportsVision: true,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const NO_VISION_CAPABILITIES: ProviderCapabilities = {
|
|
57
|
+
supportsTools: true,
|
|
58
|
+
supportsStreaming: true,
|
|
59
|
+
supportsFunctionCalling: true,
|
|
60
|
+
supportsVision: false,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function registerEchoTool(tools: ToolRegistry): void {
|
|
64
|
+
tools.register({
|
|
65
|
+
name: 'echo',
|
|
66
|
+
description: 'Echo the text back.',
|
|
67
|
+
inputSchema: z.object({ text: z.string() }),
|
|
68
|
+
execute: async () => ({ success: true, output: 'ok' }),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function baseParams(provider: LLMProvider, tools: ToolRegistry, workingDirectory: string) {
|
|
73
|
+
return {
|
|
74
|
+
provider,
|
|
75
|
+
tools,
|
|
76
|
+
runConfig: {
|
|
77
|
+
model: 'mock-model',
|
|
78
|
+
timeoutMs: 5_000,
|
|
79
|
+
tokenBudget: 100_000,
|
|
80
|
+
maxIterations: 1,
|
|
81
|
+
maxResponseTokens: 256,
|
|
82
|
+
},
|
|
83
|
+
agentId: 'agent_test',
|
|
84
|
+
agentName: 'Test Agent',
|
|
85
|
+
workingDirectory,
|
|
86
|
+
sessionId: 'ses_capability' as SessionId,
|
|
87
|
+
threadId: 'thd_capability' as ThreadId,
|
|
88
|
+
projectId: 'prj_capability' as ProjectId,
|
|
89
|
+
tenantId: 'tnt_capability' as TenantId,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe('resolveProviderCapabilities', () => {
|
|
94
|
+
it('resolves an undeclared provider to the permissive default', () => {
|
|
95
|
+
expect(resolveProviderCapabilities(new CapturingProvider())).toEqual(
|
|
96
|
+
PERMISSIVE_PROVIDER_CAPABILITIES,
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('fills a partial declaration (missing supportsVision) permissively per field', () => {
|
|
101
|
+
const resolved = resolveProviderCapabilities({
|
|
102
|
+
capabilities: {
|
|
103
|
+
supportsTools: false,
|
|
104
|
+
supportsStreaming: true,
|
|
105
|
+
supportsFunctionCalling: false,
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
expect(resolved.supportsTools).toBe(false)
|
|
109
|
+
expect(resolved.supportsVision).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('query() capability negotiation', () => {
|
|
114
|
+
let workdirs: string[] = []
|
|
115
|
+
|
|
116
|
+
afterEach(async () => {
|
|
117
|
+
await Promise.all(workdirs.map((dir) => rm(dir, { recursive: true, force: true })))
|
|
118
|
+
workdirs = []
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
async function mkWorkdir(): Promise<string> {
|
|
122
|
+
const dir = await mkdtemp(join(tmpdir(), 'namzu-capability-'))
|
|
123
|
+
workdirs.push(dir)
|
|
124
|
+
return dir
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
it('strips tool surfaces and emits a capability_warning for a no-tools provider', async () => {
|
|
128
|
+
const provider = new CapturingProvider(NO_TOOLS_CAPABILITIES)
|
|
129
|
+
const tools = new ToolRegistry()
|
|
130
|
+
registerEchoTool(tools)
|
|
131
|
+
const events: RunEvent[] = []
|
|
132
|
+
|
|
133
|
+
const run = await drainQuery(
|
|
134
|
+
{
|
|
135
|
+
...baseParams(provider, tools, await mkWorkdir()),
|
|
136
|
+
messages: [createUserMessage('hello')],
|
|
137
|
+
},
|
|
138
|
+
(event) => {
|
|
139
|
+
events.push(event)
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
expect(run.status).toBe('completed')
|
|
144
|
+
|
|
145
|
+
// The model was never told about tools: no tools param on the wire…
|
|
146
|
+
expect(provider.lastParams?.tools).toBeUndefined()
|
|
147
|
+
// …and no tool section in the system prompt.
|
|
148
|
+
const systemPrompt = (provider.lastParams?.messages ?? [])
|
|
149
|
+
.filter((m) => m.role === 'system')
|
|
150
|
+
.map((m) => m.content)
|
|
151
|
+
.join('\n')
|
|
152
|
+
expect(systemPrompt).not.toContain('<available_tools>')
|
|
153
|
+
expect(systemPrompt).not.toContain('echo')
|
|
154
|
+
|
|
155
|
+
// The host got the machine-readable warning.
|
|
156
|
+
const warning = events.find(
|
|
157
|
+
(e): e is Extract<RunEvent, { type: 'capability_warning' }> =>
|
|
158
|
+
e.type === 'capability_warning',
|
|
159
|
+
)
|
|
160
|
+
expect(warning?.capability).toBe('tools')
|
|
161
|
+
expect(warning?.providerId).toBe('capturing')
|
|
162
|
+
expect(warning?.message).toContain('stripped')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('keeps tool surfaces intact for an undeclared (permissive-default) provider', async () => {
|
|
166
|
+
const provider = new CapturingProvider()
|
|
167
|
+
const tools = new ToolRegistry()
|
|
168
|
+
registerEchoTool(tools)
|
|
169
|
+
|
|
170
|
+
const run = await drainQuery({
|
|
171
|
+
...baseParams(provider, tools, await mkWorkdir()),
|
|
172
|
+
messages: [createUserMessage('hello')],
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
expect(run.status).toBe('completed')
|
|
176
|
+
expect(provider.lastParams?.tools?.map((t) => t.function.name)).toContain('echo')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('emits a vision capability_warning when attachments hit a no-vision provider', async () => {
|
|
180
|
+
const provider = new CapturingProvider(NO_VISION_CAPABILITIES)
|
|
181
|
+
const events: RunEvent[] = []
|
|
182
|
+
|
|
183
|
+
const run = await drainQuery(
|
|
184
|
+
{
|
|
185
|
+
...baseParams(provider, new ToolRegistry(), await mkWorkdir()),
|
|
186
|
+
messages: [
|
|
187
|
+
createUserMessage('what is in this image?', [
|
|
188
|
+
{ data: 'aGVsbG8=', mediaType: 'image/png' },
|
|
189
|
+
]),
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
(event) => {
|
|
193
|
+
events.push(event)
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
expect(run.status).toBe('completed')
|
|
198
|
+
const warning = events.find(
|
|
199
|
+
(e): e is Extract<RunEvent, { type: 'capability_warning' }> =>
|
|
200
|
+
e.type === 'capability_warning',
|
|
201
|
+
)
|
|
202
|
+
expect(warning?.capability).toBe('vision')
|
|
203
|
+
expect(warning?.providerId).toBe('capturing')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('does not warn when no attachments are present on a no-vision provider', async () => {
|
|
207
|
+
const provider = new CapturingProvider(NO_VISION_CAPABILITIES)
|
|
208
|
+
const events: RunEvent[] = []
|
|
209
|
+
|
|
210
|
+
await drainQuery(
|
|
211
|
+
{
|
|
212
|
+
...baseParams(provider, new ToolRegistry(), await mkWorkdir()),
|
|
213
|
+
messages: [createUserMessage('plain text')],
|
|
214
|
+
},
|
|
215
|
+
(event) => {
|
|
216
|
+
events.push(event)
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
expect(events.some((e) => e.type === 'capability_warning')).toBe(false)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('strictCapabilities: true throws on a tools mismatch instead of degrading', async () => {
|
|
224
|
+
const provider = new CapturingProvider(NO_TOOLS_CAPABILITIES)
|
|
225
|
+
const tools = new ToolRegistry()
|
|
226
|
+
registerEchoTool(tools)
|
|
227
|
+
|
|
228
|
+
await expect(
|
|
229
|
+
drainQuery({
|
|
230
|
+
...baseParams(provider, tools, await mkWorkdir()),
|
|
231
|
+
messages: [createUserMessage('hello')],
|
|
232
|
+
strictCapabilities: true,
|
|
233
|
+
}),
|
|
234
|
+
).rejects.toThrow(/supportsTools: false/)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('strictCapabilities: true throws on a vision mismatch instead of degrading', async () => {
|
|
238
|
+
const provider = new CapturingProvider(NO_VISION_CAPABILITIES)
|
|
239
|
+
|
|
240
|
+
await expect(
|
|
241
|
+
drainQuery({
|
|
242
|
+
...baseParams(provider, new ToolRegistry(), await mkWorkdir()),
|
|
243
|
+
messages: [createUserMessage('describe', [{ data: 'aGVsbG8=', mediaType: 'image/png' }])],
|
|
244
|
+
strictCapabilities: true,
|
|
245
|
+
}),
|
|
246
|
+
).rejects.toThrow(/supportsVision: false/)
|
|
247
|
+
})
|
|
248
|
+
})
|