@namzu/sdk 58.1.0 → 59.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 (90) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/account/binding.d.ts +20 -0
  3. package/dist/account/binding.d.ts.map +1 -0
  4. package/dist/account/binding.js +27 -0
  5. package/dist/account/binding.js.map +1 -0
  6. package/dist/account/index.d.ts +9 -0
  7. package/dist/account/index.d.ts.map +1 -0
  8. package/dist/account/index.js +5 -0
  9. package/dist/account/index.js.map +1 -0
  10. package/dist/account/lock.d.ts +33 -0
  11. package/dist/account/lock.d.ts.map +1 -0
  12. package/dist/account/lock.js +170 -0
  13. package/dist/account/lock.js.map +1 -0
  14. package/dist/account/registry.d.ts +21 -0
  15. package/dist/account/registry.d.ts.map +1 -0
  16. package/dist/account/registry.js +340 -0
  17. package/dist/account/registry.js.map +1 -0
  18. package/dist/account/types.d.ts +124 -0
  19. package/dist/account/types.d.ts.map +1 -0
  20. package/dist/account/types.js +35 -0
  21. package/dist/account/types.js.map +1 -0
  22. package/dist/pals/communication/ingress-schema.d.ts +38 -38
  23. package/dist/peers/record.d.ts +98 -0
  24. package/dist/peers/record.d.ts.map +1 -1
  25. package/dist/peers/record.js +17 -0
  26. package/dist/peers/record.js.map +1 -1
  27. package/dist/peers/registry.d.ts.map +1 -1
  28. package/dist/peers/registry.js +2 -2
  29. package/dist/peers/registry.js.map +1 -1
  30. package/dist/provider/errors.d.ts.map +1 -1
  31. package/dist/provider/errors.js +25 -2
  32. package/dist/provider/errors.js.map +1 -1
  33. package/dist/provider/token-budget.d.ts.map +1 -1
  34. package/dist/provider/token-budget.js +9 -1
  35. package/dist/provider/token-budget.js.map +1 -1
  36. package/dist/public-runtime.d.ts +1 -0
  37. package/dist/public-runtime.d.ts.map +1 -1
  38. package/dist/public-runtime.js +1 -0
  39. package/dist/public-runtime.js.map +1 -1
  40. package/dist/public-types.d.ts +2 -0
  41. package/dist/public-types.d.ts.map +1 -1
  42. package/dist/runtime/query/executor.d.ts +7 -2
  43. package/dist/runtime/query/executor.d.ts.map +1 -1
  44. package/dist/runtime/query/executor.js +10 -2
  45. package/dist/runtime/query/executor.js.map +1 -1
  46. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +7 -7
  47. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
  48. package/dist/runtime/query/iteration/phases/compaction-breaker.js +7 -7
  49. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
  50. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  51. package/dist/runtime/query/tool-output-budget.js +5 -3
  52. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  53. package/dist/tools/text-window.d.ts.map +1 -1
  54. package/dist/tools/text-window.js +4 -3
  55. package/dist/tools/text-window.js.map +1 -1
  56. package/dist/types/errors/catalog.d.ts.map +1 -1
  57. package/dist/types/errors/catalog.js +6 -0
  58. package/dist/types/errors/catalog.js.map +1 -1
  59. package/dist/types/provider/errors.d.ts +7 -0
  60. package/dist/types/provider/errors.d.ts.map +1 -1
  61. package/dist/types/provider/errors.js +27 -3
  62. package/dist/types/provider/errors.js.map +1 -1
  63. package/dist/types/session/records.d.ts +215 -1
  64. package/dist/types/session/records.d.ts.map +1 -1
  65. package/dist/types/session/records.js +13 -0
  66. package/dist/types/session/records.js.map +1 -1
  67. package/dist/utils/copy-string.d.ts +10 -0
  68. package/dist/utils/copy-string.d.ts.map +1 -0
  69. package/dist/utils/copy-string.js +12 -0
  70. package/dist/utils/copy-string.js.map +1 -0
  71. package/package.json +1 -1
  72. package/src/account/binding.ts +43 -0
  73. package/src/account/index.ts +32 -0
  74. package/src/account/lock.ts +184 -0
  75. package/src/account/registry.ts +413 -0
  76. package/src/account/types.ts +137 -0
  77. package/src/peers/record.ts +18 -0
  78. package/src/peers/registry.ts +2 -2
  79. package/src/provider/errors.ts +27 -1
  80. package/src/provider/token-budget.ts +9 -3
  81. package/src/public-runtime.ts +17 -0
  82. package/src/public-types.ts +15 -0
  83. package/src/runtime/query/executor.ts +10 -2
  84. package/src/runtime/query/iteration/phases/compaction-breaker.ts +7 -7
  85. package/src/runtime/query/tool-output-budget.ts +8 -4
  86. package/src/tools/text-window.ts +4 -3
  87. package/src/types/errors/catalog.ts +6 -0
  88. package/src/types/provider/errors.ts +40 -3
  89. package/src/types/session/records.ts +16 -0
  90. package/src/utils/copy-string.ts +11 -0
@@ -800,6 +800,21 @@ export type {
800
800
  ResolvePeerRuntimeDirOptions,
801
801
  } from './peers/dir.js'
802
802
  export type { PeerRecord } from './peers/record.js'
803
+ export type { AccountBoundRecord } from './types/session/records.js'
804
+ export type {
805
+ Account,
806
+ AccountBoundReason,
807
+ AccountCredentialReaders,
808
+ AccountId,
809
+ AccountLockOptions,
810
+ AccountLockSyncOptions,
811
+ AccountRegistry,
812
+ AccountRegistryOptions,
813
+ AccountSource,
814
+ BoundAccount,
815
+ NewAccount,
816
+ ResolvedAccountCredential,
817
+ } from './account/index.js'
803
818
  export type {
804
819
  ListLivePeersOptions,
805
820
  PeerLivenessOptions,
@@ -13,6 +13,7 @@ import { buildProbeContext } from '../../probe/context.js'
13
13
  import { ProbeVetoError } from '../../probe/errors.js'
14
14
  import { probe as defaultProbeRegistry } from '../../probe/registry.js'
15
15
  import type { ProbeEnforcement } from '../../probe/registry.js'
16
+ import { ToolResultHalted } from '../../registry/tool/screen.js'
16
17
  import type { ActivityStore } from '../../store/activity/memory.js'
17
18
  import {
18
19
  effectiveBashWorkingDirectory,
@@ -3131,11 +3132,16 @@ export class ToolExecutor {
3131
3132
  }
3132
3133
 
3133
3134
  /**
3134
- * One execution attempt, with a throw materialized as an error result.
3135
+ * One execution attempt, with a throw materialized as an error result —
3136
+ * except `ToolResultHalted`, which this deliberately lets through.
3135
3137
  *
3136
3138
  * an unhandled throw from `tools.execute(...)` used to
3137
3139
  * propagate up to `result.ts` as `turn_failed` without emitting a
3138
- * terminal `tool_completed`, leaving UI cards stuck in `executing`.
3140
+ * terminal `tool_completed`, leaving UI cards stuck in `executing`. That
3141
+ * is still right for a tool that merely broke. It is wrong for a screen
3142
+ * that said `halt`: `screen.ts` throws `ToolResultHalted` instead of
3143
+ * returning it so a catch this generic cannot flatten it into a failed
3144
+ * `ToolResult` the model reads and works around.
3139
3145
  *
3140
3146
  * The return is the full `ToolResult`, not a narrowed literal: the
3141
3147
  * narrow version silently DROPPED `content`, so a tool returning an
@@ -3151,6 +3157,8 @@ export class ToolExecutor {
3151
3157
  try {
3152
3158
  return await this.executeWithDeadline(toolName, input, toolContext, prepared)
3153
3159
  } catch (err) {
3160
+ if (err instanceof ToolResultHalted) throw err
3161
+
3154
3162
  const message = toErrorMessage(err)
3155
3163
  this.log.warn('Tool execution threw', {
3156
3164
  [NAMZU.TURN_ID]: this.config.turnId,
@@ -8,9 +8,9 @@
8
8
  * line and a `breaker_open` event, no sentence); a pass the provider forces
9
9
  * by rejecting the prompt is not gated, so the reactive ladder still runs.
10
10
  * - Three compactions in a row, each followed by the history regaining all
11
- * space shed within three iterations: something in the loop (a huge file
12
- * read, a tool that prints without limit) refills the window faster than a
13
- * summary can empty it. The turn stops, because carrying on buys the same wall.
11
+ * space shed within three iterations: conversation growth refills the space
12
+ * freed. Even small outputs can do this when a summary leaves little room.
13
+ * The turn stops, because carrying on buys the same wall.
14
14
  *
15
15
  * Off unless `compactionConfig.circuitBreaker` is true: stopping compaction,
16
16
  * and above all stopping a turn, is not something a host had before.
@@ -49,12 +49,12 @@ export const COMPACTION_REFILL_ITERATIONS = 3
49
49
  export const COMPACTION_REFILL_LIMIT = 3
50
50
 
51
51
  /**
52
- * One short line, shown once, when the turn stops on refills. It names the
53
- * cause and a step that keeps the conversation: starting over is never
54
- * offered here.
52
+ * Shown once when the turn stops on refills. Explain context pressure without
53
+ * inferring the size of any individual output; suggest steps that preserve
54
+ * the conversation.
55
55
  */
56
56
  export const COMPACTION_REFILL_MESSAGE =
57
- 'The context filled up again right after being compacted, three times in a row. A file being read or a tool output is probably too large for the context window. Try reading it in smaller parts.'
57
+ 'The context filled up again right after being compacted, three times in a row. Compaction may have left too little room, even for small outputs. Check the configured model context window; limit tool output or read files in smaller parts.'
58
58
 
59
59
  export class CompactionBreaker {
60
60
  private failures = 0
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto'
2
2
  import { mkdirSync, writeFileSync } from 'node:fs'
3
3
  import { join } from 'node:path'
4
4
  import { digest, spillManifest } from '../../store/evidence/format.js'
5
+ import { copyString } from '../../utils/copy-string.js'
5
6
 
6
7
  /**
7
8
  * Model-visible size cap for a single tool result.
@@ -304,8 +305,9 @@ export function applyToolOutputBudget(opts: ApplyToolOutputBudgetOptions): ToolO
304
305
  if (preview !== undefined && retained?.integrity) {
305
306
  const condensed = [preview, recovery].filter(Boolean).join('\n\n')
306
307
  if (effectiveTextBudget === undefined || condensed.length <= effectiveTextBudget) {
308
+ const visible = withNotice(condensed)
307
309
  return {
308
- output: withNotice(condensed),
310
+ output: effectiveTextBudget === undefined ? visible : copyString(visible),
309
311
  originalLength,
310
312
  truncated: true,
311
313
  spillPath,
@@ -318,8 +320,10 @@ export function applyToolOutputBudget(opts: ApplyToolOutputBudgetOptions): ToolO
318
320
  }
319
321
 
320
322
  return {
321
- output: withNotice(
322
- boundedPreview(output, effectiveTextBudget ?? textBudget, opts.toolName, recovery),
323
+ output: copyString(
324
+ withNotice(
325
+ boundedPreview(output, effectiveTextBudget ?? textBudget, opts.toolName, recovery),
326
+ ),
323
327
  ),
324
328
  originalLength,
325
329
  truncated: true,
@@ -471,7 +475,7 @@ function previewForMessageBudget(
471
475
  spilledAt,
472
476
  ].join('\n')
473
477
  return {
474
- output,
478
+ output: copyString(output),
475
479
  originalLength: item.output.length,
476
480
  spillPath: (spilledAt.split('\n')[0] as string).slice(SPILL_MARKER.length + 1),
477
481
  }
@@ -1,4 +1,5 @@
1
1
  import type { ToolTextWindow } from '../types/tool/presentation.js'
2
+ import { copyString } from '../utils/copy-string.js'
2
3
 
3
4
  /** Lines kept from the start and from the end of a long text. */
4
5
  export const WINDOW_HEAD_LINES = 80
@@ -36,7 +37,7 @@ export function windowText(
36
37
  const lines = body.length === 0 ? [] : body.split('\n')
37
38
  const totalLines = lines.length
38
39
  if (totalLines <= WINDOW_HEAD_LINES + WINDOW_TAIL_LINES && body.length <= WINDOW_HALF_CHARS * 2) {
39
- return { head: body, totalLines, totalBytes }
40
+ return { head: copyString(body), totalLines, totalBytes }
40
41
  }
41
42
  const headLines = lines.slice(0, WINDOW_HEAD_LINES)
42
43
  const tailLines =
@@ -47,8 +48,8 @@ export function windowText(
47
48
  const tail = clipTail(tailLines.join('\n'), WINDOW_HALF_CHARS)
48
49
  const omittedLines = totalLines - headLines.length - tailLines.length
49
50
  return {
50
- head,
51
- ...(tail ? { tail } : {}),
51
+ head: copyString(head),
52
+ ...(tail ? { tail: copyString(tail) } : {}),
52
53
  ...(omittedLines > 0 ? { omittedLines } : {}),
53
54
  totalLines,
54
55
  totalBytes,
@@ -164,6 +164,12 @@ export const DEFAULT_ERROR_RULES: readonly ErrorCatalogRule[] = [
164
164
  message: 'The provider rejected the credentials for this turn.',
165
165
  hint: 'Check that the API key is set, is not expired, and belongs to the account the model is billed to. Retrying will not help until it changes.',
166
166
  },
167
+ {
168
+ id: 'provider.usage_limit',
169
+ when: (facts) => facts.code === 'usage_limit',
170
+ message: 'The account has used up its allowance until the limit resets.',
171
+ hint: 'Waiting inside this turn cannot help. Continue on another account or provider, or resume after the reset time the provider reported.',
172
+ },
167
173
  {
168
174
  id: 'provider.rate_limit',
169
175
  when: (facts) => facts.code === 'rate_limit' || facts.status === 429,
@@ -15,6 +15,12 @@
15
15
  export type ProviderErrorCode =
16
16
  /** 429. Back off; honour `retryAfterMs` when the server sent one. */
17
17
  | 'rate_limit'
18
+ /**
19
+ * The account's usage allowance is spent until a later reset, as opposed to
20
+ * a momentary burst limit. Waiting inside this turn cannot help, so it is
21
+ * not retried; a host may offer another account instead.
22
+ */
23
+ | 'usage_limit'
18
24
  /** 529 / 503 — the model is up but saturated. Back off. */
19
25
  | 'overloaded'
20
26
  /** Any other 5xx. Back off. */
@@ -487,7 +493,10 @@ const STRUCTURAL_CODES: Readonly<Record<string, ProviderErrorCode>> = {
487
493
  string_above_max_length: 'context_length_exceeded',
488
494
  rate_limit_exceeded: 'rate_limit',
489
495
  rate_limit_error: 'rate_limit',
490
- insufficient_quota: 'rate_limit',
496
+ insufficient_quota: 'usage_limit',
497
+ usage_limit_reached: 'usage_limit',
498
+ usage_limit_exceeded: 'usage_limit',
499
+ billing_error: 'usage_limit',
491
500
  overloaded_error: 'overloaded',
492
501
  content_filter: 'content_filter',
493
502
  content_policy_violation: 'content_filter',
@@ -497,6 +506,24 @@ const STRUCTURAL_CODES: Readonly<Record<string, ProviderErrorCode>> = {
497
506
  model_not_found: 'not_found',
498
507
  }
499
508
 
509
+ /**
510
+ * Is this exact provider code, or the provider's own complaint, a spent
511
+ * allowance? The codes are the vendors' identifiers (`insufficient_quota`,
512
+ * `usage_limit_reached`, `usage_limit_exceeded`, `billing_error`); the
513
+ * sentences are the two vendors that word it only in prose: an exhausted
514
+ * prepaid balance and an exhausted plan allowance.
515
+ */
516
+ const USAGE_LIMIT_DETAIL: readonly RegExp[] = [
517
+ /credit balance is too low/i,
518
+ /exceeded your current quota/i,
519
+ /usage limit (?:has been )?(?:reached|exceeded)/i,
520
+ ]
521
+
522
+ export function isUsageLimitSignal(code: string | undefined, detail: string | undefined): boolean {
523
+ if (code !== undefined && STRUCTURAL_CODES[code.toLowerCase()] === 'usage_limit') return true
524
+ return detail !== undefined && USAGE_LIMIT_DETAIL.some((re) => re.test(detail))
525
+ }
526
+
500
527
  function codeFromStructure(err: unknown): ProviderErrorCode | undefined {
501
528
  for (const link of causeChain(err)) {
502
529
  for (const field of [link.code, link.type, (link.error as { type?: unknown })?.type]) {
@@ -556,6 +583,8 @@ function classifyFromProviderRequestError(
556
583
  const candidate = err as Error & {
557
584
  kind?: unknown
558
585
  providerId?: unknown
586
+ providerCode?: unknown
587
+ detail?: unknown
559
588
  status?: unknown
560
589
  retryAfterMs?: unknown
561
590
  shouldRetry?: unknown
@@ -563,15 +592,23 @@ function classifyFromProviderRequestError(
563
592
  if (typeof candidate.kind !== 'string') return undefined
564
593
  const mapped = KIND_TO_CODE[candidate.kind]
565
594
  if (!mapped) return undefined
595
+ // A throttle or an invalid request that says the allowance is spent is not
596
+ // a burst limit: resending the same request cannot succeed.
597
+ const spent =
598
+ (candidate.kind === 'throttle' || candidate.kind === 'bad_request') &&
599
+ isUsageLimitSignal(
600
+ typeof candidate.providerCode === 'string' ? candidate.providerCode : undefined,
601
+ typeof candidate.detail === 'string' ? candidate.detail : undefined,
602
+ )
566
603
 
567
604
  const retryAfterMs =
568
605
  typeof candidate.retryAfterMs === 'number' ? candidate.retryAfterMs : readRetryAfterMs(err, now)
569
606
 
570
607
  return new ProviderError({
571
- code: mapped.code,
608
+ code: spent ? 'usage_limit' : mapped.code,
572
609
  message: err.message,
573
610
  // The provider saying "do not resend" beats the kind's own verdict.
574
- retryable: candidate.shouldRetry === false ? false : mapped.retryable,
611
+ retryable: spent || candidate.shouldRetry === false ? false : mapped.retryable,
575
612
  cause: err,
576
613
  ...(typeof candidate.providerId === 'string'
577
614
  ? { providerId: candidate.providerId }
@@ -576,6 +576,19 @@ export const BudgetBoundRecordSchema = inTurn('budget_bound', {
576
576
  accountId: text.min(1),
577
577
  })
578
578
 
579
+ /**
580
+ * The account a session's provider calls run under. Written when a session is
581
+ * bound to an account other than the provider's `default`, and again on each
582
+ * switch; never inside an open turn's work, only between turns. Replay reads
583
+ * the last one. A build that predates this record refuses a log holding it, so
584
+ * a session that never leaves the default account never writes one.
585
+ */
586
+ export const AccountBoundRecordSchema = recordSchema('account_bound', {
587
+ accountId: text.min(1),
588
+ provider: text.min(1),
589
+ reason: z.enum(['start', 'user_switch', 'limit_switch']),
590
+ })
591
+
579
592
  export const LogRepairedRecordSchema = recordSchema('log_repaired', {
580
593
  truncatedBytes: positive,
581
594
  lastGoodSeq: positive,
@@ -612,6 +625,7 @@ const RECORD_ONLY_SCHEMAS = {
612
625
  child_session_ended: ChildSessionEndedRecordSchema,
613
626
  audit: AuditRecordSchema,
614
627
  budget_bound: BudgetBoundRecordSchema,
628
+ account_bound: AccountBoundRecordSchema,
615
629
  log_repaired: LogRepairedRecordSchema,
616
630
  } as const
617
631
 
@@ -982,6 +996,7 @@ export type CompactionRecord = z.infer<typeof CompactionRecordSchema>
982
996
  export type ChildSessionEndedRecord = z.infer<typeof ChildSessionEndedRecordSchema>
983
997
  export type AuditRecord = z.infer<typeof AuditRecordSchema>
984
998
  export type BudgetBoundRecord = z.infer<typeof BudgetBoundRecordSchema>
999
+ export type AccountBoundRecord = z.infer<typeof AccountBoundRecordSchema>
985
1000
  export type LogRepairedRecord = z.infer<typeof LogRepairedRecordSchema>
986
1001
 
987
1002
  export type SessionRecordOnly =
@@ -998,6 +1013,7 @@ export type SessionRecordOnly =
998
1013
  | ChildSessionEndedRecord
999
1014
  | AuditRecord
1000
1015
  | BudgetBoundRecord
1016
+ | AccountBoundRecord
1001
1017
  | LogRepairedRecord
1002
1018
 
1003
1019
  /** One line of a session log. */
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Give retained text its own backing storage instead of keeping a source slice
3
+ * alive. Call only after bounding the text: copying a whole tool result would
4
+ * briefly double its memory. UTF-16 preserves every JavaScript code unit,
5
+ * including lone surrogates that a UTF-8 round trip would replace.
6
+ *
7
+ * @internal
8
+ */
9
+ export function copyString(text: string): string {
10
+ return Buffer.from(text, 'utf16le').toString('utf16le')
11
+ }