@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.
Files changed (153) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  3. package/dist/bridge/a2a/mapper.js +2 -0
  4. package/dist/bridge/a2a/mapper.js.map +1 -1
  5. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  6. package/dist/bridge/sse/mapper.js +4 -0
  7. package/dist/bridge/sse/mapper.js.map +1 -1
  8. package/dist/{runtime/query → constants}/continuation.d.ts +5 -0
  9. package/dist/constants/continuation.d.ts.map +1 -0
  10. package/dist/{runtime/query → constants}/continuation.js +5 -0
  11. package/dist/constants/continuation.js.map +1 -0
  12. package/dist/constants/index.d.ts +1 -0
  13. package/dist/constants/index.d.ts.map +1 -1
  14. package/dist/constants/index.js +1 -0
  15. package/dist/constants/index.js.map +1 -1
  16. package/dist/manager/run/persistence.d.ts +11 -0
  17. package/dist/manager/run/persistence.d.ts.map +1 -1
  18. package/dist/manager/run/persistence.js +30 -1
  19. package/dist/manager/run/persistence.js.map +1 -1
  20. package/dist/provider/__tests__/registry.test.d.ts +5 -0
  21. package/dist/provider/__tests__/registry.test.d.ts.map +1 -1
  22. package/dist/provider/__tests__/registry.test.js +186 -1
  23. package/dist/provider/__tests__/registry.test.js.map +1 -1
  24. package/dist/provider/capabilities.d.ts +27 -0
  25. package/dist/provider/capabilities.d.ts.map +1 -0
  26. package/dist/provider/capabilities.js +29 -0
  27. package/dist/provider/capabilities.js.map +1 -0
  28. package/dist/provider/index.d.ts +3 -1
  29. package/dist/provider/index.d.ts.map +1 -1
  30. package/dist/provider/index.js +2 -1
  31. package/dist/provider/index.js.map +1 -1
  32. package/dist/provider/registry.d.ts +81 -1
  33. package/dist/provider/registry.d.ts.map +1 -1
  34. package/dist/provider/registry.js +173 -7
  35. package/dist/provider/registry.js.map +1 -1
  36. package/dist/public-runtime.d.ts +3 -2
  37. package/dist/public-runtime.d.ts.map +1 -1
  38. package/dist/public-runtime.js +2 -2
  39. package/dist/public-runtime.js.map +1 -1
  40. package/dist/run/reporter.d.ts.map +1 -1
  41. package/dist/run/reporter.js +8 -0
  42. package/dist/run/reporter.js.map +1 -1
  43. package/dist/runtime/query/__tests__/capability-negotiation.test.d.ts +2 -0
  44. package/dist/runtime/query/__tests__/capability-negotiation.test.d.ts.map +1 -0
  45. package/dist/runtime/query/__tests__/capability-negotiation.test.js +184 -0
  46. package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -0
  47. package/dist/runtime/query/__tests__/checkpoint-store.test.d.ts +2 -0
  48. package/dist/runtime/query/__tests__/checkpoint-store.test.d.ts.map +1 -0
  49. package/dist/runtime/query/__tests__/checkpoint-store.test.js +259 -0
  50. package/dist/runtime/query/__tests__/checkpoint-store.test.js.map +1 -0
  51. package/dist/runtime/query/__tests__/checkpoint.test.js +9 -3
  52. package/dist/runtime/query/__tests__/checkpoint.test.js.map +1 -1
  53. package/dist/runtime/query/__tests__/orphan-task-turn-end.test.d.ts +2 -0
  54. package/dist/runtime/query/__tests__/orphan-task-turn-end.test.d.ts.map +1 -0
  55. package/dist/runtime/query/__tests__/orphan-task-turn-end.test.js +110 -0
  56. package/dist/runtime/query/__tests__/orphan-task-turn-end.test.js.map +1 -0
  57. package/dist/runtime/query/checkpoint.d.ts +16 -2
  58. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  59. package/dist/runtime/query/checkpoint.js +21 -7
  60. package/dist/runtime/query/checkpoint.js.map +1 -1
  61. package/dist/runtime/query/context.d.ts +7 -0
  62. package/dist/runtime/query/context.d.ts.map +1 -1
  63. package/dist/runtime/query/context.js +1 -0
  64. package/dist/runtime/query/context.js.map +1 -1
  65. package/dist/runtime/query/index.d.ts +20 -0
  66. package/dist/runtime/query/index.d.ts.map +1 -1
  67. package/dist/runtime/query/index.js +75 -6
  68. package/dist/runtime/query/index.js.map +1 -1
  69. package/dist/runtime/query/iteration/index.d.ts +3 -57
  70. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  71. package/dist/runtime/query/iteration/index.js +246 -735
  72. package/dist/runtime/query/iteration/index.js.map +1 -1
  73. package/dist/runtime/query/iteration/phases/checkpoint.d.ts.map +1 -1
  74. package/dist/runtime/query/iteration/phases/checkpoint.js +22 -0
  75. package/dist/runtime/query/iteration/phases/checkpoint.js.map +1 -1
  76. package/dist/runtime/query/iteration/phases/context.d.ts +1 -2
  77. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  78. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  79. package/dist/runtime/query/iteration/stream-turn.d.ts +35 -0
  80. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -0
  81. package/dist/runtime/query/iteration/stream-turn.js +341 -0
  82. package/dist/runtime/query/iteration/stream-turn.js.map +1 -0
  83. package/dist/runtime/query/replay/list.d.ts +17 -1
  84. package/dist/runtime/query/replay/list.d.ts.map +1 -1
  85. package/dist/runtime/query/replay/list.js +21 -4
  86. package/dist/runtime/query/replay/list.js.map +1 -1
  87. package/dist/runtime/query/replay/prepare.d.ts +10 -0
  88. package/dist/runtime/query/replay/prepare.d.ts.map +1 -1
  89. package/dist/runtime/query/replay/prepare.js +20 -4
  90. package/dist/runtime/query/replay/prepare.js.map +1 -1
  91. package/dist/store/index.d.ts +1 -0
  92. package/dist/store/index.d.ts.map +1 -1
  93. package/dist/store/index.js +1 -0
  94. package/dist/store/index.js.map +1 -1
  95. package/dist/store/run/checkpoint-disk.d.ts +28 -0
  96. package/dist/store/run/checkpoint-disk.d.ts.map +1 -0
  97. package/dist/store/run/checkpoint-disk.js +55 -0
  98. package/dist/store/run/checkpoint-disk.js.map +1 -0
  99. package/dist/types/provider/config.d.ts +51 -0
  100. package/dist/types/provider/config.d.ts.map +1 -1
  101. package/dist/types/provider/index.d.ts +1 -1
  102. package/dist/types/provider/index.d.ts.map +1 -1
  103. package/dist/types/provider/interface.d.ts +10 -0
  104. package/dist/types/provider/interface.d.ts.map +1 -1
  105. package/dist/types/run/checkpoint-store.d.ts +64 -0
  106. package/dist/types/run/checkpoint-store.d.ts.map +1 -0
  107. package/dist/types/run/checkpoint-store.js +12 -0
  108. package/dist/types/run/checkpoint-store.js.map +1 -0
  109. package/dist/types/run/config.d.ts +27 -0
  110. package/dist/types/run/config.d.ts.map +1 -1
  111. package/dist/types/run/events.d.ts +6 -0
  112. package/dist/types/run/events.d.ts.map +1 -1
  113. package/dist/types/run/events.js.map +1 -1
  114. package/dist/types/run/index.d.ts +1 -0
  115. package/dist/types/run/index.d.ts.map +1 -1
  116. package/dist/types/run/index.js +1 -0
  117. package/dist/types/run/index.js.map +1 -1
  118. package/package.json +1 -1
  119. package/src/bridge/a2a/mapper.ts +3 -0
  120. package/src/bridge/sse/mapper.ts +5 -0
  121. package/src/{runtime/query → constants}/continuation.ts +5 -0
  122. package/src/constants/index.ts +1 -0
  123. package/src/manager/run/persistence.ts +34 -1
  124. package/src/provider/__tests__/registry.test.ts +266 -1
  125. package/src/provider/capabilities.ts +47 -0
  126. package/src/provider/index.ts +12 -1
  127. package/src/provider/registry.ts +202 -7
  128. package/src/public-runtime.ts +6 -0
  129. package/src/run/reporter.ts +9 -0
  130. package/src/runtime/query/__tests__/capability-negotiation.test.ts +248 -0
  131. package/src/runtime/query/__tests__/checkpoint-store.test.ts +322 -0
  132. package/src/runtime/query/__tests__/checkpoint.test.ts +15 -6
  133. package/src/runtime/query/__tests__/orphan-task-turn-end.test.ts +127 -0
  134. package/src/runtime/query/checkpoint.ts +23 -9
  135. package/src/runtime/query/context.ts +9 -0
  136. package/src/runtime/query/index.ts +115 -28
  137. package/src/runtime/query/iteration/index.ts +298 -905
  138. package/src/runtime/query/iteration/phases/checkpoint.ts +25 -0
  139. package/src/runtime/query/iteration/phases/context.ts +1 -3
  140. package/src/runtime/query/iteration/stream-turn.ts +396 -0
  141. package/src/runtime/query/replay/list.ts +38 -4
  142. package/src/runtime/query/replay/prepare.ts +37 -4
  143. package/src/store/index.ts +1 -0
  144. package/src/store/run/checkpoint-disk.ts +67 -0
  145. package/src/types/provider/config.ts +54 -0
  146. package/src/types/provider/index.ts +3 -0
  147. package/src/types/provider/interface.ts +11 -0
  148. package/src/types/run/checkpoint-store.ts +72 -0
  149. package/src/types/run/config.ts +30 -0
  150. package/src/types/run/events.ts +13 -0
  151. package/src/types/run/index.ts +1 -0
  152. package/dist/runtime/query/continuation.d.ts.map +0 -1
  153. package/dist/runtime/query/continuation.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/run/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,wEAAwE;AACxE,4EAA4E;AAC5E,0EAA0E;AAC1E,sBAAsB;AACtB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAQ9B,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/run/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,wEAAwE;AACxE,4EAA4E;AAC5E,0EAA0E;AAC1E,sBAAsB;AACtB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAQ9B,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/sdk",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Open-source AI agent SDK with a built-in runtime. Nothing between you and your agents.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "type": "module",
@@ -60,6 +60,9 @@ const MAPPING: {
60
60
  parts: [{ kind: 'text', text: e.error }],
61
61
  }),
62
62
 
63
+ // Capability degradation is host-facing diagnostics, not A2A task state.
64
+ capability_warning: null,
65
+
63
66
  iteration_started: (e, ctx) =>
64
67
  statusEvent(e.runId, 'running', false, ctx, {
65
68
  role: 'agent',
@@ -165,6 +165,11 @@ const MAPPING: {
165
165
  run_completed: null,
166
166
  run_failed: null,
167
167
 
168
+ // Not mapped to a wire event yet — hosts consume `capability_warning`
169
+ // from the RunEvent stream directly; promoting it to the SSE contract
170
+ // needs a StreamEventType addition first.
171
+ capability_warning: null,
172
+
168
173
  agent_pending: {
169
174
  wire: 'agent.pending',
170
175
  transform: (e, runId) => ({
@@ -11,6 +11,11 @@
11
11
  * that match this constant verbatim, so the run's `result` field
12
12
  * concatenates the full multi-turn assistant output instead of only
13
13
  * surfacing the trailing continuation chunk.
14
+ *
15
+ * Lives in `constants/` (not `runtime/query/`) because both the
16
+ * runtime iteration loop and `manager/run/persistence.ts` consume
17
+ * it — a manager→runtime import for a string constant would create
18
+ * a manager↔runtime directory cycle.
14
19
  */
15
20
  export const AUTO_CONTINUATION_USER_MESSAGE =
16
21
  'Continue exactly where you left off. Do not repeat content you already wrote — pick up at the next token.'
@@ -7,6 +7,7 @@ export * from './rag/index.js'
7
7
  export * from './tools/index.js'
8
8
  export * from './telemetry/index.js'
9
9
  export * from './agent/index.js'
10
+ export * from './continuation.js'
10
11
  export * from './limits.js'
11
12
  export * from './bus/index.js'
12
13
  export * from './verification/index.js'
@@ -1,9 +1,11 @@
1
+ import { AUTO_CONTINUATION_USER_MESSAGE } from '../../constants/continuation.js'
1
2
  import { EMPTY_TOKEN_USAGE } from '../../constants/limits.js'
2
- import { AUTO_CONTINUATION_USER_MESSAGE } from '../../runtime/query/continuation.js'
3
+ import { DiskCheckpointStore } from '../../store/run/checkpoint-disk.js'
3
4
  import { RunDiskStore } from '../../store/run/disk.js'
4
5
  import { type CostInfo, type TokenUsage, accumulateTokenUsage } from '../../types/common/index.js'
5
6
  import type { RunId, SessionId, TenantId } from '../../types/ids/index.js'
6
7
  import type { Message } from '../../types/message/index.js'
8
+ import type { CheckpointRunScope, CheckpointStore } from '../../types/run/checkpoint-store.js'
7
9
  import type { EmergencySaveData } from '../../types/run/emergency.js'
8
10
  import type { Run, RunPersistenceConfig, StopReason } from '../../types/run/index.js'
9
11
  import type { ProjectId, ThreadId } from '../../types/session/ids.js'
@@ -14,6 +16,7 @@ import type { Logger } from '../../utils/logger.js'
14
16
  export class RunPersistence {
15
17
  private run: Run
16
18
  private runStore: RunDiskStore
19
+ private checkpointStore: CheckpointStore
17
20
  private pricing?: ModelPricing
18
21
  private log: Logger
19
22
  private readonly _sessionId: SessionId
@@ -34,6 +37,15 @@ export class RunPersistence {
34
37
  logger: config.log,
35
38
  })
36
39
 
40
+ // Checkpoints go through the injectable seam; the disk layout under
41
+ // `outputDir` (same tree the runStore writes to) stays the default.
42
+ this.checkpointStore =
43
+ config.checkpointStore ??
44
+ new DiskCheckpointStore({
45
+ baseDir: config.outputDir,
46
+ logger: config.log,
47
+ })
48
+
37
49
  this.run = {
38
50
  id: config.runId,
39
51
  status: 'idle',
@@ -109,6 +121,27 @@ export class RunPersistence {
109
121
  return this.runStore
110
122
  }
111
123
 
124
+ /**
125
+ * Checkpoint persistence for this run — the injected
126
+ * {@link CheckpointStore} when the host provided one, otherwise the
127
+ * disk default. Pair with {@link getRunScope} when constructing a
128
+ * `CheckpointManager`.
129
+ */
130
+ getCheckpointStore(): CheckpointStore {
131
+ return this.checkpointStore
132
+ }
133
+
134
+ /** Full five-layer scope key for this run's checkpoint operations. */
135
+ getRunScope(): CheckpointRunScope {
136
+ return {
137
+ tenantId: this._tenantId,
138
+ projectId: this._projectId,
139
+ sessionId: this._sessionId,
140
+ runId: this.run.id,
141
+ parentRunId: this.run.parentRunId,
142
+ }
143
+ }
144
+
112
145
  getRunDir(): string | null {
113
146
  return this.runStore.getRunDir()
114
147
  }
@@ -1,9 +1,16 @@
1
1
  import { beforeEach, describe, expect, it } from 'vitest'
2
- import type { LLMProvider, ProviderCapabilities } from '../../types/provider/index.js'
2
+ import type {
3
+ LLMProvider,
4
+ LazyProviderModule,
5
+ ProviderCapabilities,
6
+ } from '../../types/provider/index.js'
7
+ import { PERMISSIVE_PROVIDER_CAPABILITIES, resolveProviderCapabilities } from '../capabilities.js'
3
8
  import { registerMock } from '../mock-register.js'
4
9
  import { MockLLMProvider } from '../mock.js'
5
10
  import {
6
11
  DuplicateProviderError,
12
+ LazyProviderLoadError,
13
+ LazyProviderSyncCreateError,
7
14
  ProviderRegistry,
8
15
  UnknownProviderError,
9
16
  __resetProviderRegistryInternal,
@@ -16,9 +23,15 @@ interface TestProviderConfig {
16
23
  value?: string
17
24
  }
18
25
 
26
+ interface LazyTestProviderConfig {
27
+ type: 'lazytest'
28
+ value?: string
29
+ }
30
+
19
31
  declare module '../../types/provider/config.js' {
20
32
  interface ProviderConfigRegistry {
21
33
  test: TestProviderConfig
34
+ lazytest: LazyTestProviderConfig
22
35
  }
23
36
  }
24
37
 
@@ -52,6 +65,41 @@ const TEST_CAPS: ProviderCapabilities = {
52
65
  supportsFunctionCalling: false,
53
66
  }
54
67
 
68
+ class LazyTestProvider implements LLMProvider {
69
+ readonly id = 'lazytest'
70
+ readonly name = 'Lazy Test'
71
+ constructor(
72
+ public readonly config: LazyTestProviderConfig,
73
+ readonly capabilities?: ProviderCapabilities,
74
+ ) {}
75
+ async *chatStream() {
76
+ yield { id: 'x', delta: { content: 'ok' } }
77
+ }
78
+ }
79
+
80
+ const HINT_CAPS: ProviderCapabilities = {
81
+ supportsTools: false,
82
+ supportsStreaming: false,
83
+ supportsFunctionCalling: false,
84
+ }
85
+
86
+ const MODULE_CAPS: ProviderCapabilities = {
87
+ supportsTools: true,
88
+ supportsStreaming: true,
89
+ supportsFunctionCalling: true,
90
+ supportsVision: false,
91
+ }
92
+
93
+ function deferred<T>() {
94
+ let resolve!: (value: T) => void
95
+ let reject!: (reason?: unknown) => void
96
+ const promise = new Promise<T>((res, rej) => {
97
+ resolve = res
98
+ reject = rej
99
+ })
100
+ return { promise, resolve, reject }
101
+ }
102
+
55
103
  describe('ProviderRegistry', () => {
56
104
  beforeEach(() => {
57
105
  __resetProviderRegistryInternal()
@@ -152,4 +200,221 @@ describe('ProviderRegistry', () => {
152
200
  expect(err.name).toBe('DuplicateProviderError')
153
201
  })
154
202
  })
203
+
204
+ describe('registerLazy', () => {
205
+ it('does not invoke the loader at registration time', () => {
206
+ let calls = 0
207
+ ProviderRegistry.registerLazy('lazytest', async () => {
208
+ calls++
209
+ return { create: (config) => new LazyTestProvider(config) }
210
+ })
211
+
212
+ expect(calls).toBe(0)
213
+ expect(ProviderRegistry.isSupported('lazytest')).toBe(true)
214
+ expect(ProviderRegistry.listTypes()).toContain('lazytest')
215
+ })
216
+
217
+ it('throws DuplicateProviderError over an existing eager registration', () => {
218
+ ProviderRegistry.register('test', TestProvider, TEST_CAPS)
219
+ expect(() =>
220
+ ProviderRegistry.registerLazy('test', async () => ({
221
+ create: (config) => new TestProvider(config),
222
+ })),
223
+ ).toThrowError(DuplicateProviderError)
224
+ })
225
+
226
+ it('throws DuplicateProviderError over an existing lazy registration', () => {
227
+ const loader = async (): Promise<LazyProviderModule<LazyTestProviderConfig>> => ({
228
+ create: (config) => new LazyTestProvider(config),
229
+ })
230
+ ProviderRegistry.registerLazy('lazytest', loader)
231
+ expect(() => ProviderRegistry.registerLazy('lazytest', loader)).toThrowError(
232
+ DuplicateProviderError,
233
+ )
234
+ })
235
+
236
+ it('replace: true swaps between eager and lazy registrations', async () => {
237
+ ProviderRegistry.register('lazytest', LazyTestProvider, TEST_CAPS)
238
+ ProviderRegistry.registerLazy(
239
+ 'lazytest',
240
+ async () => ({ create: (config) => new LazyTestProvider(config) }),
241
+ { replace: true },
242
+ )
243
+ expect(() => ProviderRegistry.createProvider({ type: 'lazytest' })).toThrowError(
244
+ LazyProviderSyncCreateError,
245
+ )
246
+
247
+ ProviderRegistry.register('lazytest', LazyTestProvider, TEST_CAPS, { replace: true })
248
+ expect(ProviderRegistry.createProvider({ type: 'lazytest' })).toBeInstanceOf(LazyTestProvider)
249
+ })
250
+
251
+ it('sync create()/createProvider() always throws for lazy types, even after load', async () => {
252
+ ProviderRegistry.registerLazy('lazytest', async () => ({
253
+ create: (config) => new LazyTestProvider(config),
254
+ }))
255
+
256
+ expect(() => ProviderRegistry.create({ type: 'lazytest' })).toThrowError(
257
+ LazyProviderSyncCreateError,
258
+ )
259
+
260
+ await ProviderRegistry.createAsync({ type: 'lazytest' })
261
+ expect(() => ProviderRegistry.createProvider({ type: 'lazytest' })).toThrowError(
262
+ LazyProviderSyncCreateError,
263
+ )
264
+ })
265
+
266
+ it('unregister removes a lazy registration', () => {
267
+ ProviderRegistry.registerLazy('lazytest', async () => ({
268
+ create: (config) => new LazyTestProvider(config),
269
+ }))
270
+ expect(ProviderRegistry.unregister('lazytest')).toBe(true)
271
+ expect(ProviderRegistry.isSupported('lazytest')).toBe(false)
272
+ })
273
+ })
274
+
275
+ describe('createAsync', () => {
276
+ it('first create invokes the loader once; subsequent creates reuse the cached factory', async () => {
277
+ let calls = 0
278
+ ProviderRegistry.registerLazy('lazytest', async () => {
279
+ calls++
280
+ return { create: (config) => new LazyTestProvider(config) }
281
+ })
282
+
283
+ const first = await ProviderRegistry.createAsync({ type: 'lazytest', value: 'a' })
284
+ const second = await ProviderRegistry.createAsync({ type: 'lazytest', value: 'b' })
285
+
286
+ expect(calls).toBe(1)
287
+ expect(first.provider).toBeInstanceOf(LazyTestProvider)
288
+ expect(second.provider).toBeInstanceOf(LazyTestProvider)
289
+ expect((second.provider as LazyTestProvider).config.value).toBe('b')
290
+ })
291
+
292
+ it('concurrent first-creates share a single loader invocation', async () => {
293
+ let calls = 0
294
+ const gate = deferred<void>()
295
+ ProviderRegistry.registerLazy('lazytest', async () => {
296
+ calls++
297
+ await gate.promise
298
+ return { create: (config) => new LazyTestProvider(config) }
299
+ })
300
+
301
+ const a = ProviderRegistry.createAsync({ type: 'lazytest' })
302
+ const b = ProviderRegistry.createAsync({ type: 'lazytest' })
303
+ gate.resolve()
304
+
305
+ const [ra, rb] = await Promise.all([a, b])
306
+ expect(calls).toBe(1)
307
+ expect(ra.provider).toBeInstanceOf(LazyTestProvider)
308
+ expect(rb.provider).toBeInstanceOf(LazyTestProvider)
309
+ })
310
+
311
+ it('loader rejection surfaces as LazyProviderLoadError and the next create retries', async () => {
312
+ let calls = 0
313
+ ProviderRegistry.registerLazy('lazytest', async () => {
314
+ calls++
315
+ if (calls === 1) {
316
+ throw new Error('transient network failure')
317
+ }
318
+ return { create: (config) => new LazyTestProvider(config) }
319
+ })
320
+
321
+ const failure = await ProviderRegistry.createAsync({ type: 'lazytest' }).catch(
322
+ (err: unknown) => err,
323
+ )
324
+ expect(failure).toBeInstanceOf(LazyProviderLoadError)
325
+ expect((failure as LazyProviderLoadError).providerType).toBe('lazytest')
326
+ expect(((failure as LazyProviderLoadError).cause as Error).message).toBe(
327
+ 'transient network failure',
328
+ )
329
+
330
+ const { provider } = await ProviderRegistry.createAsync({ type: 'lazytest' })
331
+ expect(calls).toBe(2)
332
+ expect(provider).toBeInstanceOf(LazyTestProvider)
333
+ })
334
+
335
+ it('rejects with LazyProviderLoadError when the loader resolves an invalid module shape', async () => {
336
+ ProviderRegistry.registerLazy(
337
+ 'lazytest',
338
+ async () => ({}) as unknown as LazyProviderModule<LazyTestProviderConfig>,
339
+ )
340
+ await expect(ProviderRegistry.createAsync({ type: 'lazytest' })).rejects.toThrowError(
341
+ LazyProviderLoadError,
342
+ )
343
+ })
344
+
345
+ it('works for eagerly registered types too', async () => {
346
+ const { provider, capabilities } = await ProviderRegistry.createAsync({
347
+ type: 'mock',
348
+ model: 'test-model',
349
+ })
350
+ expect(provider).toBeInstanceOf(MockLLMProvider)
351
+ expect(capabilities.supportsStreaming).toBe(true)
352
+ })
353
+
354
+ it('rejects with UnknownProviderError for unregistered types', async () => {
355
+ await expect(
356
+ ProviderRegistry.createAsync({ type: 'nonexistent' } as unknown as { type: 'mock' }),
357
+ ).rejects.toThrowError(UnknownProviderError)
358
+ })
359
+ })
360
+
361
+ describe('lazy capabilities', () => {
362
+ it('answers with the registration hint before load, without invoking the loader', () => {
363
+ let calls = 0
364
+ ProviderRegistry.registerLazy(
365
+ 'lazytest',
366
+ async () => {
367
+ calls++
368
+ return { create: (config) => new LazyTestProvider(config) }
369
+ },
370
+ { capabilities: HINT_CAPS },
371
+ )
372
+
373
+ expect(ProviderRegistry.getCapabilities('lazytest')).toEqual(HINT_CAPS)
374
+ expect(calls).toBe(0)
375
+ })
376
+
377
+ it('answers permissively before load when no hint was given', () => {
378
+ ProviderRegistry.registerLazy('lazytest', async () => ({
379
+ create: (config) => new LazyTestProvider(config),
380
+ }))
381
+ expect(ProviderRegistry.getCapabilities('lazytest')).toEqual(PERMISSIVE_PROVIDER_CAPABILITIES)
382
+ })
383
+
384
+ it('module capabilities replace the hint after load', async () => {
385
+ ProviderRegistry.registerLazy(
386
+ 'lazytest',
387
+ async () => ({
388
+ create: (config) => new LazyTestProvider(config),
389
+ capabilities: MODULE_CAPS,
390
+ }),
391
+ { capabilities: HINT_CAPS },
392
+ )
393
+
394
+ const { capabilities } = await ProviderRegistry.createAsync({ type: 'lazytest' })
395
+ expect(capabilities).toEqual(MODULE_CAPS)
396
+ expect(ProviderRegistry.getCapabilities('lazytest')).toEqual(MODULE_CAPS)
397
+ })
398
+
399
+ it('the constructed instance own capabilities win at run time over any hint', async () => {
400
+ const instanceCaps: ProviderCapabilities = {
401
+ supportsTools: true,
402
+ supportsStreaming: true,
403
+ supportsFunctionCalling: false,
404
+ supportsVision: false,
405
+ }
406
+ ProviderRegistry.registerLazy(
407
+ 'lazytest',
408
+ async () => ({ create: (config) => new LazyTestProvider(config, instanceCaps) }),
409
+ { capabilities: HINT_CAPS },
410
+ )
411
+
412
+ const { provider } = await ProviderRegistry.createAsync({ type: 'lazytest' })
413
+ // The query runtime negotiates against the instance, not the registry:
414
+ const resolved = resolveProviderCapabilities(provider)
415
+ expect(resolved.supportsTools).toBe(true)
416
+ expect(resolved.supportsFunctionCalling).toBe(false)
417
+ expect(resolved.supportsVision).toBe(false)
418
+ })
419
+ })
155
420
  })
@@ -0,0 +1,47 @@
1
+ import type { LLMProvider, ProviderCapabilities } from '../types/provider/index.js'
2
+
3
+ /**
4
+ * Fully-resolved capability set: every flag present, no optionals left.
5
+ * What the query runtime branches on after applying the permissive default.
6
+ */
7
+ export interface ResolvedProviderCapabilities {
8
+ supportsTools: boolean
9
+ supportsStreaming: boolean
10
+ supportsFunctionCalling: boolean
11
+ supportsVision: boolean
12
+ maxOutputTokens?: number
13
+ }
14
+
15
+ /**
16
+ * THE permissive default: a provider that declares nothing is assumed to
17
+ * handle everything — exactly the pre-negotiation behavior, so third-party
18
+ * providers written before `LLMProvider.capabilities` existed keep working
19
+ * without a warning storm. Shipped drivers always declare explicitly.
20
+ */
21
+ export const PERMISSIVE_PROVIDER_CAPABILITIES: ResolvedProviderCapabilities = {
22
+ supportsTools: true,
23
+ supportsStreaming: true,
24
+ supportsFunctionCalling: true,
25
+ supportsVision: true,
26
+ }
27
+
28
+ /**
29
+ * Resolve a provider's declared capabilities against the permissive
30
+ * default. Absent declaration ⇒ everything `true`; a partial declaration
31
+ * (e.g. one missing `supportsVision`, added later) fills the gap
32
+ * permissively per field.
33
+ */
34
+ export function resolveProviderCapabilities(
35
+ provider: Pick<LLMProvider, 'capabilities'>,
36
+ ): ResolvedProviderCapabilities {
37
+ const declared: ProviderCapabilities | undefined = provider.capabilities
38
+ return {
39
+ supportsTools: declared?.supportsTools ?? PERMISSIVE_PROVIDER_CAPABILITIES.supportsTools,
40
+ supportsStreaming:
41
+ declared?.supportsStreaming ?? PERMISSIVE_PROVIDER_CAPABILITIES.supportsStreaming,
42
+ supportsFunctionCalling:
43
+ declared?.supportsFunctionCalling ?? PERMISSIVE_PROVIDER_CAPABILITIES.supportsFunctionCalling,
44
+ supportsVision: declared?.supportsVision ?? PERMISSIVE_PROVIDER_CAPABILITIES.supportsVision,
45
+ maxOutputTokens: declared?.maxOutputTokens,
46
+ }
47
+ }
@@ -1,3 +1,14 @@
1
- export { ProviderRegistry, UnknownProviderError, DuplicateProviderError } from './registry.js'
1
+ export {
2
+ ProviderRegistry,
3
+ UnknownProviderError,
4
+ DuplicateProviderError,
5
+ LazyProviderLoadError,
6
+ LazyProviderSyncCreateError,
7
+ } from './registry.js'
2
8
  export { MockLLMProvider } from './mock.js'
3
9
  export { registerMock, MOCK_CAPABILITIES } from './mock-register.js'
10
+ export {
11
+ PERMISSIVE_PROVIDER_CAPABILITIES,
12
+ resolveProviderCapabilities,
13
+ } from './capabilities.js'
14
+ export type { ResolvedProviderCapabilities } from './capabilities.js'