@namzu/sdk 16.0.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  3. package/dist/bridge/a2a/mapper.js +13 -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 +16 -0
  7. package/dist/bridge/sse/mapper.js.map +1 -1
  8. package/dist/contracts/api.d.ts +8 -1
  9. package/dist/contracts/api.d.ts.map +1 -1
  10. package/dist/provider/fallback.d.ts +93 -0
  11. package/dist/provider/fallback.d.ts.map +1 -0
  12. package/dist/provider/fallback.js +265 -0
  13. package/dist/provider/fallback.js.map +1 -0
  14. package/dist/provider/index.d.ts +2 -0
  15. package/dist/provider/index.d.ts.map +1 -1
  16. package/dist/provider/index.js +1 -0
  17. package/dist/provider/index.js.map +1 -1
  18. package/dist/public-runtime.d.ts +2 -2
  19. package/dist/public-runtime.d.ts.map +1 -1
  20. package/dist/public-runtime.js +1 -1
  21. package/dist/public-runtime.js.map +1 -1
  22. package/dist/run/reporter.d.ts.map +1 -1
  23. package/dist/run/reporter.js +16 -0
  24. package/dist/run/reporter.js.map +1 -1
  25. package/dist/runtime/query/index.d.ts +17 -0
  26. package/dist/runtime/query/index.d.ts.map +1 -1
  27. package/dist/runtime/query/index.js +56 -3
  28. package/dist/runtime/query/index.js.map +1 -1
  29. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  30. package/dist/runtime/query/iteration/stream-turn.js +25 -0
  31. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  32. package/dist/types/provider/stream.d.ts +35 -0
  33. package/dist/types/provider/stream.d.ts.map +1 -1
  34. package/dist/types/run/events.d.ts +29 -0
  35. package/dist/types/run/events.d.ts.map +1 -1
  36. package/dist/types/run/events.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/bridge/a2a/mapper.ts +16 -0
  39. package/src/bridge/sse/mapper.ts +17 -0
  40. package/src/contracts/api.ts +7 -0
  41. package/src/provider/fallback.ts +300 -0
  42. package/src/provider/index.ts +2 -0
  43. package/src/public-runtime.ts +7 -1
  44. package/src/run/reporter.ts +20 -0
  45. package/src/runtime/query/index.ts +80 -3
  46. package/src/runtime/query/iteration/stream-turn.ts +26 -0
  47. package/src/types/provider/stream.ts +37 -0
  48. package/src/types/run/events.ts +29 -0
@@ -16,6 +16,7 @@ import type { CompactionConfig } from '../../config/runtime.js'
16
16
  import { TOOL_OUTPUT_DIR_NAME } from '../../constants/tools/index.js'
17
17
  import { EmergencySaveManager } from '../../manager/run/emergency.js'
18
18
  import { resolveProviderCapabilities } from '../../provider/capabilities.js'
19
+ import { type ProviderChainMember, withProviderFallback } from '../../provider/fallback.js'
19
20
  import { type ProviderRetryConfig, withProviderRetry } from '../../provider/retry.js'
20
21
  import type { PathBuilder } from '../../session/workspace/path-builder.js'
21
22
  import {
@@ -120,6 +121,23 @@ export interface QueryParams {
120
121
  */
121
122
  retry?: Partial<ProviderRetryConfig> | false
122
123
 
124
+ /**
125
+ * Members to fall over to, in order, when {@link provider} cannot serve.
126
+ *
127
+ * Absent means what it always meant: one provider, no failover. Each member
128
+ * is tried at most once per call and the chain never rewinds, so the scope
129
+ * of a swap is this `query()` — for a host whose call is one user turn, that
130
+ * is turn scope with no reset to forget. See `withProviderFallback`.
131
+ *
132
+ * Two things this does NOT do, both deliberate. Capabilities are negotiated
133
+ * once against {@link provider}, so a member that declares less will be sent
134
+ * a request shaped for the head — refuse a disagreeing chain before you
135
+ * build one. And a fallback loses the prompt cache: the replacement provider
136
+ * has never seen this conversation, so the turn re-reads its whole context
137
+ * at full price.
138
+ */
139
+ fallbackProviders?: readonly ProviderChainMember[]
140
+
123
141
  /**
124
142
  * Install process-level crash handlers that dump this run's state to
125
143
  * `<runDir>/../emergency/<runId>.json` on SIGINT, SIGTERM or an
@@ -472,6 +490,46 @@ export interface QueryParams {
472
490
  strictCapabilities?: boolean
473
491
  }
474
492
 
493
+ /**
494
+ * Refuse to price a run whose tokens two differently-priced members may produce.
495
+ *
496
+ * `RunPersistence` holds ONE {@link ModelPricing} table and applies it to every
497
+ * accumulation regardless of which model produced the tokens. Across a swap that
498
+ * makes `costInfo.totalCost` wrong by an unbounded margin, and silently — the
499
+ * number keeps the shape of an answer. `CostInfo` cannot express the truth
500
+ * either: it carries `inputCostPer1M` / `outputCostPer1M`, and there is no
501
+ * honest value for those once a total spans two rate cards.
502
+ *
503
+ * So the total is refused rather than blended. Naming what that costs is part
504
+ * of the refusal, because the caller loses `costLimitUsd` with it: the guard
505
+ * enforces that limit from this same accumulated total, and a limit enforced
506
+ * with the wrong rate card stops a run early or late by the same unbounded
507
+ * margin. A budget that is quietly wrong is worse than a budget that is
508
+ * declined.
509
+ *
510
+ * Reachable, not decorative: a host that passes `pricing` and declares a chain
511
+ * hits it on the first call. It costs `@namzu/cli` nothing, which passes no
512
+ * pricing at all — its `/cost` already reports that the provider gave no price.
513
+ *
514
+ * The way out is per-member pricing, which needs a `CostInfo` that can sum over
515
+ * heterogeneous rates. That is a public-type change and it is not this one.
516
+ */
517
+ function assertCostIsAttributable(
518
+ chain: readonly ProviderChainMember[],
519
+ pricing: ModelPricing | undefined,
520
+ ): void {
521
+ if (pricing === undefined || chain.length < 2) return
522
+ throw new NamzuError({
523
+ code: 'invalid_config',
524
+ message:
525
+ `A provider chain of ${chain.length} members was declared together with a single pricing table. ` +
526
+ 'One table cannot price two members, so the run would report a total that is wrong by an unbounded ' +
527
+ 'margin — and `runConfig.costLimitUsd` would be enforced against that same wrong total. ' +
528
+ 'Either drop `pricing` (usage is still reported per model in the run) or declare one member.',
529
+ details: { chainLength: chain.length },
530
+ })
531
+ }
532
+
475
533
  export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run> {
476
534
  // Boot-time filesystem migration (session-hierarchy.md §13.4.1). First
477
535
  // call per process per root actually runs; subsequent calls short-circuit
@@ -489,10 +547,29 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
489
547
  // of its warns behind `options.log`, and this is its only production
490
548
  // call site — so without it the "failed, retrying" and "failed, giving
491
549
  // up" lines were dead code and a backoff left no trace anywhere.
492
- const resilientProvider =
550
+ //
551
+ // With a chain declared, the same sentence holds one level out: retry is
552
+ // applied per MEMBER and the fallback decorator wraps the result, so the
553
+ // composition is `fallback(retry(m0), retry(m1), …)`. That order is not a
554
+ // preference. Assembled the other way round — which is what a host gets if
555
+ // it wraps its own chain and hands the result in, because this function
556
+ // would then wrap THAT in retry — an exhausted chain gets restarted from
557
+ // the head by the outer loop and a throttle on the last member is counted
558
+ // by two budgets. Building it here is what makes the order unspellable
559
+ // wrong.
560
+ const chain: readonly ProviderChainMember[] = [
561
+ { provider: params.provider },
562
+ ...(params.fallbackProviders ?? []),
563
+ ]
564
+ assertCostIsAttributable(chain, params.pricing)
565
+ const withRetry = (provider: LLMProvider): LLMProvider =>
493
566
  params.retry === false
494
- ? params.provider
495
- : withProviderRetry(params.provider, { config: params.retry, log: getRootLogger() })
567
+ ? provider
568
+ : withProviderRetry(provider, { config: params.retry, log: getRootLogger() })
569
+ const resilientProvider = withProviderFallback(
570
+ chain.map((member) => ({ ...member, provider: withRetry(member.provider) })),
571
+ { log: getRootLogger() },
572
+ )
496
573
 
497
574
  const ctx = RunContextFactory.build({
498
575
  agentId: params.agentId,
@@ -258,6 +258,32 @@ export async function* streamProviderTurn(
258
258
  continue
259
259
  }
260
260
 
261
+ // A chain swap, not output, and handled beside the retry notice
262
+ // because it is the same kind of thing: a fact about HOW the answer
263
+ // is being produced, arriving on the only channel open while the
264
+ // consumer is blocked inside the provider's iterator. It carries no
265
+ // delta, so nothing below applies to it either.
266
+ if (chunk.fallback) {
267
+ await emitEvent({
268
+ type: 'provider_fallback',
269
+ runId,
270
+ iteration,
271
+ fromIndex: chunk.fallback.fromIndex,
272
+ fromProviderId: chunk.fallback.fromProviderId,
273
+ ...(chunk.fallback.fromModel !== undefined
274
+ ? { fromModel: chunk.fallback.fromModel }
275
+ : {}),
276
+ toIndex: chunk.fallback.toIndex,
277
+ toProviderId: chunk.fallback.toProviderId,
278
+ ...(chunk.fallback.toModel !== undefined ? { toModel: chunk.fallback.toModel } : {}),
279
+ code: chunk.fallback.code,
280
+ ...(chunk.fallback.status !== undefined ? { status: chunk.fallback.status } : {}),
281
+ reason: chunk.fallback.reason,
282
+ })
283
+ yield* drainPending()
284
+ continue
285
+ }
286
+
261
287
  if (chunk.error) {
262
288
  streamError = chunk.error
263
289
  break
@@ -81,6 +81,18 @@ export interface StreamChunk {
81
81
  * output.
82
82
  */
83
83
  retry?: ProviderRetryNotice
84
+
85
+ /**
86
+ * The call failed and a different member of the provider chain is taking
87
+ * over from here.
88
+ *
89
+ * Emitted by the fallback decorator, never by a driver, and it rides the
90
+ * stream for the reason {@link retry} does. Like a retry notice it carries
91
+ * no delta and must not be treated as output — and that distinction is
92
+ * load-bearing twice over, because the fallback decorator reads these
93
+ * chunks too when deciding whether output has already gone out.
94
+ */
95
+ fallback?: ProviderFallbackNotice
84
96
  }
85
97
 
86
98
  /** See {@link StreamChunk.retry}. */
@@ -96,3 +108,28 @@ export interface ProviderRetryNotice {
96
108
  /** The delay came from the server's own `Retry-After`, not backoff. */
97
109
  readonly serverDirected: boolean
98
110
  }
111
+
112
+ /**
113
+ * See {@link StreamChunk.fallback}.
114
+ *
115
+ * Both members are named, not just the new one. Naming only the replacement
116
+ * does not tell an operator which of their declared members went down, and on a
117
+ * chain of four that is the only fact they can act on.
118
+ *
119
+ * The positions are 0-based indices into the chain as the host declared it, so
120
+ * a surface can name a member the way its own configuration does rather than
121
+ * inventing a second numbering.
122
+ */
123
+ export interface ProviderFallbackNotice {
124
+ readonly fromIndex: number
125
+ readonly fromProviderId: string
126
+ readonly fromModel?: string
127
+ readonly toIndex: number
128
+ readonly toProviderId: string
129
+ readonly toModel?: string
130
+ /** Classified failure code, as `classifyProviderError` reports it. */
131
+ readonly code: string
132
+ readonly status?: number
133
+ /** The classified failure's own sentence, already redacted at its source. */
134
+ readonly reason: string
135
+ }
@@ -186,6 +186,35 @@ type CoreRunEvent =
186
186
  /** The delay came from the server's own `Retry-After`. */
187
187
  serverDirected: boolean
188
188
  }
189
+ /**
190
+ * A member of the provider chain could not serve, and a later member has
191
+ * taken over. The run continues from where it stopped.
192
+ *
193
+ * This event is the feature's honesty. A chain that swapped silently would
194
+ * produce a run that succeeded while quietly not doing what the operator
195
+ * asked — served by a provider they did not choose, at a price and a
196
+ * quality they did not agree to, with nothing in the transcript saying so.
197
+ * A host is expected to SHOW this, not log it.
198
+ *
199
+ * Emitted at the moment of the swap, before the replacement request runs.
200
+ */
201
+ | {
202
+ type: 'provider_fallback'
203
+ runId: RunId
204
+ iteration: number
205
+ /** 0-based position in the chain, as the host declared it. */
206
+ fromIndex: number
207
+ fromProviderId: string
208
+ fromModel?: string
209
+ toIndex: number
210
+ toProviderId: string
211
+ toModel?: string
212
+ /** Classified failure code, as the boundary classifier reports it. */
213
+ code: string
214
+ status?: number
215
+ /** The classified failure's own sentence. */
216
+ reason: string
217
+ }
189
218
  | {
190
219
  type: 'tool_completed'
191
220
  runId: RunId