@namzu/sdk 21.1.0 → 22.1.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 (91) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/dist/advisory/executor.d.ts +9 -4
  3. package/dist/advisory/executor.d.ts.map +1 -1
  4. package/dist/advisory/executor.js +16 -9
  5. package/dist/advisory/executor.js.map +1 -1
  6. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  7. package/dist/agents/SupervisorAgent.js +12 -0
  8. package/dist/agents/SupervisorAgent.js.map +1 -1
  9. package/dist/constants/limits.d.ts +13 -0
  10. package/dist/constants/limits.d.ts.map +1 -1
  11. package/dist/constants/limits.js +14 -2
  12. package/dist/constants/limits.js.map +1 -1
  13. package/dist/manager/agent/lifecycle.d.ts +2 -2
  14. package/dist/manager/run/persistence.d.ts +37 -2
  15. package/dist/manager/run/persistence.d.ts.map +1 -1
  16. package/dist/manager/run/persistence.js +48 -7
  17. package/dist/manager/run/persistence.js.map +1 -1
  18. package/dist/pricing/catalogue.generated.d.ts +15 -0
  19. package/dist/pricing/catalogue.generated.d.ts.map +1 -0
  20. package/dist/pricing/catalogue.generated.js +293 -0
  21. package/dist/pricing/catalogue.generated.js.map +1 -0
  22. package/dist/pricing/index.d.ts +46 -0
  23. package/dist/pricing/index.d.ts.map +1 -0
  24. package/dist/pricing/index.js +70 -0
  25. package/dist/pricing/index.js.map +1 -0
  26. package/dist/public-runtime.d.ts +2 -1
  27. package/dist/public-runtime.d.ts.map +1 -1
  28. package/dist/public-runtime.js +6 -1
  29. package/dist/public-runtime.js.map +1 -1
  30. package/dist/public-types.d.ts +3 -1
  31. package/dist/public-types.d.ts.map +1 -1
  32. package/dist/run/LimitChecker.d.ts +10 -0
  33. package/dist/run/LimitChecker.d.ts.map +1 -1
  34. package/dist/run/LimitChecker.js +14 -2
  35. package/dist/run/LimitChecker.js.map +1 -1
  36. package/dist/runtime/query/checkpoint.d.ts +23 -2
  37. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  38. package/dist/runtime/query/checkpoint.js +24 -3
  39. package/dist/runtime/query/checkpoint.js.map +1 -1
  40. package/dist/runtime/query/guard.d.ts.map +1 -1
  41. package/dist/runtime/query/guard.js +1 -0
  42. package/dist/runtime/query/guard.js.map +1 -1
  43. package/dist/runtime/query/index.d.ts.map +1 -1
  44. package/dist/runtime/query/index.js +42 -0
  45. package/dist/runtime/query/index.js.map +1 -1
  46. package/dist/runtime/query/iteration/index.d.ts +44 -8
  47. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  48. package/dist/runtime/query/iteration/index.js +71 -13
  49. package/dist/runtime/query/iteration/index.js.map +1 -1
  50. package/dist/runtime/query/iteration/phases/advisory.d.ts.map +1 -1
  51. package/dist/runtime/query/iteration/phases/advisory.js +9 -1
  52. package/dist/runtime/query/iteration/phases/advisory.js.map +1 -1
  53. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  54. package/dist/runtime/query/iteration/phases/compaction.js +11 -2
  55. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  56. package/dist/store/run/conformance.d.ts.map +1 -1
  57. package/dist/store/run/conformance.js +8 -2
  58. package/dist/store/run/conformance.js.map +1 -1
  59. package/dist/types/agent/supervisor.d.ts +29 -0
  60. package/dist/types/agent/supervisor.d.ts.map +1 -1
  61. package/dist/types/common/index.d.ts +38 -2
  62. package/dist/types/common/index.d.ts.map +1 -1
  63. package/dist/types/run/stop-reason.d.ts +31 -1
  64. package/dist/types/run/stop-reason.d.ts.map +1 -1
  65. package/dist/utils/cost.d.ts +60 -2
  66. package/dist/utils/cost.d.ts.map +1 -1
  67. package/dist/utils/cost.js +126 -11
  68. package/dist/utils/cost.js.map +1 -1
  69. package/package.json +1 -1
  70. package/src/advisory/executor.ts +16 -9
  71. package/src/agents/SupervisorAgent.ts +12 -0
  72. package/src/constants/limits.ts +14 -2
  73. package/src/manager/agent/lifecycle.ts +2 -2
  74. package/src/manager/run/persistence.ts +67 -7
  75. package/src/pricing/catalogue.generated.ts +308 -0
  76. package/src/pricing/index.ts +78 -0
  77. package/src/pricing/rates.source.json +206 -0
  78. package/src/public-runtime.ts +8 -0
  79. package/src/public-types.ts +3 -1
  80. package/src/run/LimitChecker.ts +24 -2
  81. package/src/runtime/query/checkpoint.ts +24 -3
  82. package/src/runtime/query/guard.ts +1 -0
  83. package/src/runtime/query/index.ts +42 -0
  84. package/src/runtime/query/iteration/index.ts +76 -13
  85. package/src/runtime/query/iteration/phases/advisory.ts +9 -1
  86. package/src/runtime/query/iteration/phases/compaction.ts +12 -2
  87. package/src/store/run/conformance.ts +8 -2
  88. package/src/types/agent/supervisor.ts +30 -0
  89. package/src/types/common/index.ts +38 -2
  90. package/src/types/run/stop-reason.ts +30 -0
  91. package/src/utils/cost.ts +186 -16
@@ -43,11 +43,32 @@ export function toCheckpointListEntry(cp: IterationCheckpoint): CheckpointListEn
43
43
  * the same restore path as any other checkpoint.
44
44
  *
45
45
  * The projection is lossy: `costInfo`, `guardState.elapsedMs` and
46
- * `toolResultHashes` are not captured at emergency-save time and default
47
- * to zero/empty values. The synthetic
46
+ * `toolResultHashes` are not captured at emergency-save time. The synthetic
48
47
  * checkpoint id is derived deterministically from the emergency save id so
49
48
  * re-projecting the same dump yields the same {@link CheckpointId}.
50
49
  *
50
+ * ## The cost projection says "unknown", not "zero"
51
+ *
52
+ * `costInfo` used to default to `{ ...ZERO_COST }` beside a `tokenUsage` the
53
+ * dump preserves faithfully — a run that had spent real money coming back as
54
+ * one that had spent nothing. That is the same lie the price catalogue was
55
+ * added to end, arriving through the restore path instead of the accumulation
56
+ * one, and it took `runConfig.costLimitUsd` back to being enforced against a
57
+ * zero with it.
58
+ *
59
+ * It also broke the accumulator downstream. `accumulateCost` decides whether
60
+ * one rate card describes the whole total by asking whether the total is
61
+ * FRESH — zero cost, zero unpriced tokens, no rates — and `ZERO_COST` is
62
+ * exactly that shape. So the first turn after such a resume adopted its own
63
+ * rate card as covering the pre-crash spend too, and reported a number that
64
+ * was confidently wrong while looking like a measured one.
65
+ *
66
+ * Saying instead that the pre-crash tokens are UNPRICED is the true statement,
67
+ * in the vocabulary {@link CostInfo} already has: nobody knows what they cost,
68
+ * because nothing recorded it. It needs no marker of its own to stay correct,
69
+ * and it is not fresh-shaped, so the accumulator does the right thing without
70
+ * being told about this path at all.
71
+ *
51
72
  * See ses_005-deterministic-replay design §2 + §5.2.
52
73
  */
53
74
  export function projectEmergencyToCheckpoint(dump: EmergencySaveData): IterationCheckpoint {
@@ -64,7 +85,7 @@ export function projectEmergencyToCheckpoint(dump: EmergencySaveData): Iteration
64
85
  iteration: dump.currentIteration,
65
86
  messages: dump.messages,
66
87
  tokenUsage: dump.tokenUsage,
67
- costInfo: { ...ZERO_COST },
88
+ costInfo: { ...ZERO_COST, unpricedTokens: dump.tokenUsage.totalTokens },
68
89
  guardState: {
69
90
  iterationCount: dump.currentIteration,
70
91
  elapsedMs: Math.max(0, dump.savedAt - dump.startedAt),
@@ -92,6 +92,7 @@ export class GuardCoordinator {
92
92
  aborted: abortSignal.aborted,
93
93
  totalTokens: runMgr.tokenUsage.totalTokens,
94
94
  totalCost: runMgr.costInfo.totalCost,
95
+ unpricedTokens: runMgr.costInfo.unpricedTokens,
95
96
  currentIteration: runMgr.currentIteration,
96
97
  startTime: this.startTime,
97
98
  }
@@ -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 type { RunPersistence } from '../../manager/run/persistence.js'
19
+ import { resolveModelPricing } from '../../pricing/index.js'
19
20
  import { resolveProviderCapabilities } from '../../provider/capabilities.js'
20
21
  import {
21
22
  type ProviderChainMember,
@@ -607,6 +608,46 @@ function assertCostIsAttributable(
607
608
  })
608
609
  }
609
610
 
611
+ /**
612
+ * Refuse a budget that cannot be measured.
613
+ *
614
+ * `runConfig.costLimitUsd` is enforced against `costInfo.totalCost`, and that
615
+ * total only moves for tokens something has a rate for. A model no rate card
616
+ * covers therefore produced a limit that could never trip — a host that set a
617
+ * cost cap had no cost cap, and nothing said so. That was every run before the
618
+ * price catalogue existed, which is how it went unnoticed.
619
+ *
620
+ * Refusing at the front is the cheap half of the answer: it costs the caller
621
+ * nothing, fires before any spend, and names both ways out. The other half is
622
+ * the `cost_unmeasurable` stop, for the models this cannot see — a step naming
623
+ * its own, or a chain member declaring one.
624
+ *
625
+ * This is the same shape `advisory/budget.ts` already applies to
626
+ * `AdvisoryBudget.maxCostPerRun`, one layer down, and for the same reason. The
627
+ * run path simply never had it.
628
+ */
629
+ function assertBudgetIsMeasurable(params: QueryParams): void {
630
+ const limit = params.runConfig.costLimitUsd
631
+ if (limit === undefined || limit <= 0) return
632
+ // A host-supplied table prices whatever it is pointed at, so a caller who
633
+ // brought one has answered the question themselves.
634
+ if (params.pricing !== undefined) return
635
+ const model = params.runConfig.model
636
+ if (resolveModelPricing(params.provider.id, model) !== undefined) return
637
+
638
+ throw new NamzuError({
639
+ code: 'invalid_config',
640
+ message:
641
+ `runConfig.costLimitUsd is set to ${limit}, but no rate is known for model "${model}" on ` +
642
+ `provider "${params.provider.id}". The limit is enforced against the run's accumulated ` +
643
+ 'cost, and tokens with no rate never reach that total — so the budget would read as ' +
644
+ 'satisfied for the whole run and stop nothing. Either pass `pricing` to declare the rate ' +
645
+ 'yourself, add the model to packages/sdk/src/pricing/rates.source.json, or drop ' +
646
+ '`costLimitUsd` and bound the run with `tokenBudget`, which is measurable here.',
647
+ details: { model, providerId: params.provider.id, costLimitUsd: limit },
648
+ })
649
+ }
650
+
610
651
  export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run> {
611
652
  // Boot-time filesystem migration (session-hierarchy.md §13.4.1). First
612
653
  // call per process per root actually runs; subsequent calls short-circuit
@@ -639,6 +680,7 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
639
680
  ...(params.fallbackProviders ?? []),
640
681
  ]
641
682
  assertCostIsAttributable(chain, params.pricing)
683
+ assertBudgetIsMeasurable(params)
642
684
  const withRetry = (provider: LLMProvider): LLMProvider =>
643
685
  params.retry === false
644
686
  ? provider
@@ -461,7 +461,17 @@ export class IterationOrchestrator {
461
461
  })()
462
462
 
463
463
  // Main-loop turn: also records the prompt size compaction reads.
464
- runMgr.recordTurnUsage(response.usage)
464
+ //
465
+ // `servedBy` is what prices it, and it is the exact pair —
466
+ // the member at the cursor when the stream ended, and the
467
+ // model it was asked for. This is the seam that ended the
468
+ // always-zero cost: the rate lookup happens per turn,
469
+ // against who actually answered, rather than against one
470
+ // table the run was constructed with.
471
+ runMgr.recordTurnUsage(response.usage, {
472
+ providerId: servedBy.providerId,
473
+ model: servedBy.model,
474
+ })
465
475
 
466
476
  // The turn went through, so the run is not sitting on an
467
477
  // irreducible prompt any more. Re-arm relief for the next one.
@@ -862,8 +872,9 @@ export class IterationOrchestrator {
862
872
 
863
873
  // A successful `structured_output` call IS the answer, so the
864
874
  // run ends here rather than paying for another turn whose only
865
- // job would be to restate it.
866
- if (this.captureStructuredOutput(reviewOutcome.results)) {
875
+ // job would be to restate it — unless it shared its turn with
876
+ // other calls, which relays instead. See the method.
877
+ if (this.captureStructuredOutput(reviewOutcome.results, response)) {
867
878
  this.ctx.log.info('Structured output produced — ending run', {
868
879
  runId: runMgr.id,
869
880
  iteration: iterationNum,
@@ -1357,14 +1368,6 @@ export class IterationOrchestrator {
1357
1368
  return this.ctx.structuredOutput !== undefined && !this.structuredOutputDone
1358
1369
  }
1359
1370
 
1360
- /**
1361
- * Record the structured output if this batch produced one.
1362
- *
1363
- * The tool validates against the Zod schema before its `execute` runs,
1364
- * so reaching here successfully means the value is already valid — a
1365
- * failed parse comes back as an error result and simply does not
1366
- * satisfy the demand, which sends the loop round again.
1367
- */
1368
1371
  /**
1369
1372
  * The answer a terminal tool produced, or `undefined` to keep looping.
1370
1373
  *
@@ -1405,10 +1408,67 @@ export class IterationOrchestrator {
1405
1408
  return hit
1406
1409
  }
1407
1410
 
1408
- private captureStructuredOutput(results: readonly ToolCallOutcome[]): boolean {
1411
+ /**
1412
+ * Record the structured output if this batch produced one.
1413
+ *
1414
+ * The tool validates against the Zod schema before its `execute` runs,
1415
+ * so reaching here successfully means the value is already valid — a
1416
+ * failed parse comes back as an error result and simply does not
1417
+ * satisfy the demand, which sends the loop round again.
1418
+ *
1419
+ * Narrow in the same way {@link terminalToolOutput} is, for its stated
1420
+ * reason and one that is sharper here. The neighbour refuses a shared
1421
+ * turn because "a model that asked for other work meant to see those
1422
+ * results". That applies unchanged. But the batch has ALREADY executed
1423
+ * by the time this runs — `runToolReview` settles it, side effects
1424
+ * included, before either of these is consulted — so settling here is
1425
+ * worse than discarding an answer the model wanted: the work happened,
1426
+ * its results went into the transcript, and the run ended before any
1427
+ * model turn could read them. Nothing consumed what was spent, and
1428
+ * nothing said so.
1429
+ *
1430
+ * Sharper, too, because of WHEN this value was produced. The model
1431
+ * emitted its final answer in the same turn as a request for
1432
+ * information it had not yet received — it would not have asked
1433
+ * otherwise — so the answer is under-informed on the model's own
1434
+ * account, and settling ships it as final.
1435
+ *
1436
+ * So: relay, do not settle. The results are already in the transcript,
1437
+ * the demand is still unsatisfied, and the next turn produces the
1438
+ * answer with them in hand. Refusing to EXECUTE the batch was the other
1439
+ * candidate and is wrong — the defect is not that the tools ran, it is
1440
+ * that nobody read them, and denying a model work it asked for to
1441
+ * protect an answer it has not finished forming gives up a real
1442
+ * capability for nothing. The price is one extra turn when the paired
1443
+ * call was a pure side effect whose result the model did not need;
1444
+ * that is the price `terminalToolOutput` already pays, and a model
1445
+ * avoids it by not pairing.
1446
+ *
1447
+ * NOT charged to `maxRetries`. That budget bounds a model that cannot
1448
+ * satisfy the SCHEMA, and this one did. A run reading two files a turn
1449
+ * while optimistically attaching its answer is making progress, and it
1450
+ * must not die reported as `structured_output_failed` — a failure that
1451
+ * did not happen. `maxIterations` is the bound for a model that keeps
1452
+ * doing work, and it is the bound the neighbour relies on for the
1453
+ * identical pathology.
1454
+ */
1455
+ private captureStructuredOutput(
1456
+ results: readonly ToolCallOutcome[],
1457
+ response: ChatCompletionResponse,
1458
+ ): boolean {
1409
1459
  if (!this.needsStructuredOutput()) return false
1410
1460
  const hit = results.find((r) => r.toolName === STRUCTURED_OUTPUT_TOOL_NAME && !r.isError)
1411
1461
  if (!hit) return false
1462
+
1463
+ const callCount = response.message.toolCalls?.length ?? 0
1464
+ if (callCount > 1) {
1465
+ this.ctx.log.info('Structured output shared its turn — relaying instead of settling', {
1466
+ runId: this.ctx.runMgr.id,
1467
+ callsInTurn: callCount,
1468
+ })
1469
+ return false
1470
+ }
1471
+
1412
1472
  try {
1413
1473
  this.ctx.runMgr.setStructuredOutput(JSON.parse(hit.output))
1414
1474
  } catch {
@@ -1529,7 +1589,10 @@ export class IterationOrchestrator {
1529
1589
  }),
1530
1590
  )
1531
1591
 
1532
- this.ctx.runMgr.accumulateUsage(response.usage)
1592
+ this.ctx.runMgr.accumulateUsage(response.usage, {
1593
+ providerId: this.ctx.runMgr.servingProviderId,
1594
+ model,
1595
+ })
1533
1596
 
1534
1597
  const assistantMsg = createAssistantMessage(response.message.content)
1535
1598
  this.ctx.runMgr.pushMessage(assistantMsg)
@@ -117,7 +117,15 @@ export async function runAdvisoryPhase(
117
117
  // `tokenBudget: 200_000` and an `on_error` trigger could send well
118
118
  // past 200k and never trip `token_budget`. The usage is already in
119
119
  // hand — this just tells the accountant about it.
120
- ctx.runMgr.accumulateUsage(executionResult.usage)
120
+ // Priced against the ADVISOR's own driver and model, not the run's. An
121
+ // advisor carries its own `provider`, so attributing its tokens to
122
+ // whoever is serving the main loop would price one vendor's work at
123
+ // another's card — which is the class of quiet wrongness the whole
124
+ // catalogue exists to remove, and it would be invisible here.
125
+ ctx.runMgr.accumulateUsage(executionResult.usage, {
126
+ providerId: advisor.provider.id,
127
+ model: advisor.model,
128
+ })
121
129
 
122
130
  advisoryCtx.recordCall({
123
131
  advisorId: advisor.id,
@@ -542,19 +542,29 @@ export async function runCompactionCheck(
542
542
  let compactedContent: string
543
543
 
544
544
  if (config.llmVerification && manager.slotCount() < config.richStateThreshold) {
545
+ // Named once and used twice on purpose. The model the summariser is
546
+ // asked for is also the model its tokens are priced at, and computing
547
+ // the two separately is how they drift — a router that sends compaction
548
+ // to a cheap model while the bill is written against the expensive one
549
+ // is a mistake with no symptom.
550
+ const compactionModel = resolveTaskModel('compaction', ctx.taskRouter, ctx.runConfig.model)
545
551
  compactedContent = await buildVerifiedSummary(
546
552
  manager,
547
553
  olderMessages,
548
554
  ctx.provider,
549
555
  config,
550
- (usage) => ctx.runMgr.accumulateUsage(usage),
556
+ (usage) =>
557
+ ctx.runMgr.accumulateUsage(usage, {
558
+ providerId: ctx.runMgr.servingProviderId,
559
+ model: compactionModel,
560
+ }),
551
561
  // The one model call a run makes that the user never asked for. It
552
562
  // reads a transcript and writes a summary, which is the cheapest
553
563
  // thing a small model does well, and it fires on exactly the long
554
564
  // runs where the primary model is most expensive. `taskRouter` had
555
565
  // been accepted, validated and threaded through four types since it
556
566
  // was added, and nothing ever consulted it.
557
- resolveTaskModel('compaction', ctx.taskRouter, ctx.runConfig.model),
567
+ compactionModel,
558
568
  )
559
569
  } else {
560
570
  compactedContent = serializeState(manager.getState())
@@ -177,11 +177,17 @@ const FOREIGN_TENANT = 'tnt_conformance_other' as TenantId
177
177
  /** Fixed instant, so every expiry in a case is judged against one clock. */
178
178
  const NOW = 5_000_000
179
179
 
180
+ /**
181
+ * A run that cost nothing and has nothing unaccounted for — the shape a store
182
+ * has to round-trip, not a claim about any real run. Carries no rate fields,
183
+ * matching `ZERO_COST`: a store fixture that invents `inputCostPer1M: 0` would
184
+ * be asserting a rate card of zero, which now means "this model is free"
185
+ * rather than "no rate was applied".
186
+ */
180
187
  const NO_COST: CostInfo = {
181
- inputCostPer1M: 0,
182
- outputCostPer1M: 0,
183
188
  totalCost: 0,
184
189
  cacheDiscount: 0,
190
+ unpricedTokens: 0,
185
191
  }
186
192
 
187
193
  let checkpointSeq = 0
@@ -6,6 +6,7 @@ import type { LLMProvider } from '../provider/index.js'
6
6
  import type { TaskRouterConfig } from '../router/index.js'
7
7
  import type { SandboxProvider } from '../sandbox/index.js'
8
8
  import type { Skill } from '../skills/index.js'
9
+ import type { StructuredOutputConfig } from '../structured-output/index.js'
9
10
  import type { ToolRegistryContract } from '../tool/index.js'
10
11
  import type { VerificationGateConfig } from '../verification/index.js'
11
12
  import type { BaseAgentConfig, BaseAgentResult } from './base.js'
@@ -172,6 +173,35 @@ export interface SupervisorAgentConfig extends BaseAgentConfig {
172
173
  */
173
174
  compactionConfig?: CompactionConfig
174
175
 
176
+ /**
177
+ * Demand that the supervisor's own final answer match a schema.
178
+ *
179
+ * `ReactiveAgent` has forwarded this since the field existed and the
180
+ * supervisor never took it, with nothing in this file saying why —
181
+ * which in a file where `maxDepth`, `allowDelegation`,
182
+ * `maxToolConcurrency` and `siblingFailurePolicy` each carry a
183
+ * paragraph of argument is the signature of an oversight, not of a
184
+ * decision. The kernel path is archetype-blind: `drainQuery` registers
185
+ * `structured_output` from this config and the loop captures it, so the
186
+ * capability was always there and only the hop was missing.
187
+ *
188
+ * **What it buys, exactly.** Structured output is terminal and
189
+ * exclusive by policy: `setStructuredOutput` overwrites `Run.result`
190
+ * behind a sticky flag and the run ends on the turn that produces it.
191
+ * So this gives a supervisor a schema-constrained FINAL ANSWER and
192
+ * nothing more. It does not shape a delegated child's answer — a child
193
+ * carries its own config — it does not run alongside prose, and it is
194
+ * not a return type for the fan-out. A host wanting typed results from
195
+ * the workers sets the schema on the workers.
196
+ *
197
+ * One consequence a supervisor host in particular should know: the
198
+ * answer decides the run, so delegated work still running when it lands
199
+ * is walked away from rather than waited for. It is recorded — the run
200
+ * names it on `abandonedTaskIds` — but it is not delivered. That is the
201
+ * same precedence a terminal tool has, stated in the iteration loop.
202
+ */
203
+ structuredOutput?: StructuredOutputConfig
204
+
175
205
  /**
176
206
  * Optional neutral working-memory seam. When set, the SDK re-renders the
177
207
  * provider's string into a single pinned leading system message every
@@ -74,10 +74,46 @@ export function mergeTokenUsage(current: TokenUsage, next: TokenUsage): TokenUsa
74
74
  }
75
75
 
76
76
  export interface CostInfo {
77
- inputCostPer1M: number
78
- outputCostPer1M: number
77
+ /**
78
+ * The input rate this total was accumulated at — present only when ONE rate
79
+ * card describes the whole total.
80
+ *
81
+ * Absent means "no single rate describes this", not "zero". Three ways to
82
+ * get there: nothing has been accumulated yet, the run spanned two
83
+ * differently-priced models, or part of it was accumulated at no known rate
84
+ * at all. This is the same contract {@link TokenUsage.reasoningTokens} uses
85
+ * one field up, for the same reason — a number invented to fill the slot
86
+ * would be indistinguishable from a measured one.
87
+ *
88
+ * The field used to be required, and a run that swapped models reported
89
+ * whichever card was applied last, which is a claim about the whole total
90
+ * that was true of only part of it.
91
+ */
92
+ inputCostPer1M?: number
93
+ /** As {@link CostInfo.inputCostPer1M}, for output tokens. */
94
+ outputCostPer1M?: number
79
95
  totalCost: number
80
96
  cacheDiscount: number
97
+ /**
98
+ * Tokens accumulated at no known rate, so `totalCost` does not include what
99
+ * they cost.
100
+ *
101
+ * This exists so a consumer can tell "this run cost nothing" from "nobody
102
+ * knows what this run cost". Reporting the second as zero is the defect the
103
+ * price catalogue was added to fix, one level down: a total that is always
104
+ * zero and a total that is zero because it is unknown look identical, and
105
+ * `runConfig.costLimitUsd` is enforced against both.
106
+ *
107
+ * - `totalCost: 0, unpricedTokens: 0` — the run genuinely cost nothing
108
+ * (local inference bills per token exactly never).
109
+ * - `totalCost: 0, unpricedTokens: 4210` — nobody knows.
110
+ * - `totalCost: 0.12, unpricedTokens: 900` — partly known; the total is a
111
+ * floor, not the answer.
112
+ *
113
+ * A count rather than a flag because a run mixes turns: a step can name its
114
+ * own model and a provider chain can swap members mid-run.
115
+ */
116
+ unpricedTokens: number
81
117
  }
82
118
 
83
119
  export interface PlatformError {
@@ -2,6 +2,36 @@ export type StopReason =
2
2
  | 'end_turn'
3
3
  | 'token_budget'
4
4
  | 'cost_limit'
5
+ /**
6
+ * A `costLimitUsd` was set and part of the run ran at a rate nobody has, so
7
+ * the limit could not be measured.
8
+ *
9
+ * Distinct from `cost_limit`, and the distinction is the reason this value
10
+ * exists: nothing was overspent. Reporting `cost_limit` would send the
11
+ * reader to look at spend that was never computed, and would hide the one
12
+ * fact they need — that the budget they configured was unenforceable for
13
+ * part of this run.
14
+ *
15
+ * `query()` refuses the same combination up front, so this is the case
16
+ * preflight cannot see: a step naming its own model, or a chain member
17
+ * declaring one, arriving at a model the price catalogue has no row for.
18
+ * `costInfo.unpricedTokens` says how much of the run it covers.
19
+ *
20
+ * ## The closing call is outside the budget, deliberately
21
+ *
22
+ * Every hard stop is followed by one more model call — `requestFinalResponse`
23
+ * asks for a closing summary, and the guard does not run again before or
24
+ * after it. That is pre-existing and true of `cost_limit` and
25
+ * `token_budget` alike, and it is stated here rather than left to be
26
+ * discovered because for THIS reason it cannot be otherwise: the model that
27
+ * triggered the stop is by definition one with no rate, so the closing
28
+ * call's cost is unmeasurable by construction. Bounding it would mean
29
+ * refusing to close the run at all, which loses the work.
30
+ *
31
+ * Its tokens are counted, so `costInfo.unpricedTokens` includes them and
32
+ * the run reports honestly what it could not price.
33
+ */
34
+ | 'cost_unmeasurable'
5
35
  | 'timeout'
6
36
  | 'max_iterations'
7
37
  | 'cancelled'