@gajae-code/ai 0.15.3 → 0.15.5

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.
@@ -1,5 +1,8 @@
1
+ import type { Effort } from "../model-thinking";
1
2
  import type { StreamFunction, StreamOptions } from "../types";
2
3
  export interface KiroCodeWhispererOptions extends StreamOptions {
4
+ /** Effort level for Kiro API-key reasoning. */
5
+ reasoning?: Effort | boolean;
3
6
  /** AWS region for the CodeWhisperer streaming endpoint. */
4
7
  region?: string;
5
8
  /** Profile ARN for enterprise IAM Identity Center accounts. */
@@ -11,6 +11,7 @@
11
11
  * not from any AGPL reference implementation.
12
12
  */
13
13
  import { $credentialEnv, $env, extractHttpStatusFromError } from "@gajae-code/utils";
14
+ import type { Effort } from "../model-thinking";
14
15
  import type {
15
16
  Api,
16
17
  AssistantMessage,
@@ -28,12 +29,15 @@ import { transportFailureFacts } from "../utils/fallback-transport";
28
29
  import { withHttpStatus } from "../utils/http-inspector";
29
30
  import { captureUnicodeEscapeEvidence } from "../utils/json-parse";
30
31
  import { decodeEventStream } from "./aws-eventstream";
32
+ import { isKiroApiKey, streamKiroApiKey } from "./kiro-api-key";
31
33
 
32
34
  // ─────────────────────────────────────────────────────────────────────────────
33
35
  // Provider options
34
36
  // ─────────────────────────────────────────────────────────────────────────────
35
37
 
36
38
  export interface KiroCodeWhispererOptions extends StreamOptions {
39
+ /** Effort level for Kiro API-key reasoning. */
40
+ reasoning?: Effort | boolean;
37
41
  /** AWS region for the CodeWhisperer streaming endpoint. */
38
42
  region?: string;
39
43
  /** Profile ARN for enterprise IAM Identity Center accounts. */
@@ -143,6 +147,11 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
143
147
  context: Context,
144
148
  options: KiroCodeWhispererOptions,
145
149
  ): AssistantMessageEventStream => {
150
+ const token = resolveBearerToken(options.apiKey);
151
+ if (isKiroApiKey(token)) {
152
+ return streamKiroApiKey(model, context, { ...options, apiKey: token });
153
+ }
154
+
146
155
  const stream = new AssistantMessageEventStream();
147
156
 
148
157
  (async () => {
@@ -175,7 +184,7 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
175
184
  const bearerToken = resolveBearerToken(options.apiKey);
176
185
  if (!bearerToken) {
177
186
  throw new Error(
178
- "No Kiro credentials found. Run 'gjc auth-broker login kiro' to authenticate via AWS Builder ID, or set AWS_BEARER_TOKEN_KIRO.",
187
+ "No Kiro credentials found. Set KIRO_API_KEY (ksk_ from https://app.kiro.dev/settings/api-keys) or run 'gjc auth-broker login kiro'.",
179
188
  );
180
189
  }
181
190
 
@@ -541,7 +550,7 @@ function handleToolUseEvent(
541
550
 
542
551
  function resolveBearerToken(apiKey: string | undefined): string | undefined {
543
552
  if (!apiKey) {
544
- return $credentialEnv("AWS_BEARER_TOKEN_KIRO") ?? undefined;
553
+ return $credentialEnv("KIRO_API_KEY") ?? $credentialEnv("AWS_BEARER_TOKEN_KIRO") ?? undefined;
545
554
  }
546
555
 
547
556
  // Structured API key (from getOAuthApiKey) contains the access token as JSON
@@ -1571,6 +1571,8 @@ function handleOutputItemDone(
1571
1571
  arguments: terminalArguments as Record<string, unknown>,
1572
1572
  };
1573
1573
  captureUnicodeEscapeEvidence(toolCall, item.arguments);
1574
+ delete runtime.currentBlock.escapedNonAsciiArguments;
1575
+ delete runtime.currentBlock.escapedUnicodeArgumentEvidence;
1574
1576
  Object.assign(runtime.currentBlock, toolCall);
1575
1577
  captureUnicodeEscapeEvidence(runtime.currentBlock, item.arguments);
1576
1578
  delete (runtime.currentBlock as { partialJson?: string }).partialJson;
@@ -1625,6 +1627,8 @@ function handleOutputItemDone(
1625
1627
  arguments: { input: terminalInput },
1626
1628
  customWireName: item.name,
1627
1629
  };
1630
+ delete runtime.currentBlock.escapedNonAsciiArguments;
1631
+ delete runtime.currentBlock.escapedUnicodeArgumentEvidence;
1628
1632
  Object.assign(runtime.currentBlock, toolCall);
1629
1633
  delete (runtime.currentBlock as { partialJson?: string }).partialJson;
1630
1634
  delete (runtime.currentBlock as { doneInput?: string }).doneInput;
@@ -31,6 +31,7 @@ import {
31
31
  } from "../types";
32
32
  import { normalizeResponsesToolCallId, sanitizeJsonStrings } from "../utils";
33
33
  import type { AssistantMessageEventStream } from "../utils/event-stream";
34
+ import { SERVER_OVERLOADED_PROVIDER_CODE } from "../utils/fallback-transport";
34
35
  import { captureUnicodeEscapeEvidence, isCompleteJson, parseStreamingJson } from "../utils/json-parse";
35
36
  import { areJsonValuesEqual } from "../utils/schema";
36
37
  import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER, partitionVisionContent } from "./vision-guard";
@@ -981,7 +982,10 @@ export async function processResponsesStream<TApi extends Api>(
981
982
  : typeof statusDetailsReason === "string" && statusDetailsReason.length > 0
982
983
  ? `status_details: ${statusDetailsReason}`
983
984
  : "Unknown error (no error details in response)";
984
- throw new Error(message);
985
+ // A `cancelled` response is not a capacity rejection and may already
986
+ // have produced observable work, so only the `failed` status carries
987
+ // the typed code onward.
988
+ throw createResponsesFailedError(message, response.status === "failed" ? error?.code : undefined);
985
989
  }
986
990
  // A response cut short for length (`incomplete`) may have stopped
987
991
  // mid-tool-call. Any tool-call item still tracked in `items` never
@@ -1003,11 +1007,28 @@ export async function processResponsesStream<TApi extends Api>(
1003
1007
  : details?.reason
1004
1008
  ? `incomplete: ${details.reason}`
1005
1009
  : "Unknown error (no error details in response)";
1006
- throw new Error(message);
1010
+ throw createResponsesFailedError(message, error?.code);
1007
1011
  }
1008
1012
  }
1009
1013
  }
1010
1014
 
1015
+ /**
1016
+ * A terminal failure envelope arrives inside an HTTP 200 stream — as
1017
+ * `response.failed`, or as `response.completed` with a `failed` response status
1018
+ * — so the typed `error.code` is the only structured evidence the transport can
1019
+ * keep. Both shapes carry the same structured failure and are typed identically.
1020
+ * Exactly OpenAI's capacity-overload code is carried through as transport facts,
1021
+ * matched case-sensitively; every other failure stays a plain error, so an
1022
+ * untyped, cased, or malformed code can never reach a typed retry admission. The
1023
+ * display message is unchanged either way.
1024
+ */
1025
+ function createResponsesFailedError(message: string, code: string | undefined): Error {
1026
+ if (code !== SERVER_OVERLOADED_PROVIDER_CODE) return new Error(message);
1027
+ const error = new Error(message) as Error & { openaiErrorCode?: string };
1028
+ error.openaiErrorCode = SERVER_OVERLOADED_PROVIDER_CODE;
1029
+ return error;
1030
+ }
1031
+
1011
1032
  /**
1012
1033
  * Whether two raw JSON argument strings decode to the same value. Used to tell a
1013
1034
  * relay's re-serialization of the same tool arguments apart from a genuine
package/src/stream.ts CHANGED
@@ -131,10 +131,12 @@ const serviceProviderMap: Record<string, KeyResolver> = {
131
131
  tavily: "TAVILY_API_KEY",
132
132
  parallel: "PARALLEL_API_KEY",
133
133
  kagi: "KAGI_API_KEY",
134
- // Kiro uses AWS SSO OIDC OAuth flow; bearer token is stored as the OAuth access token.
134
+ // Kiro API keys use the ksk_ prefix; preserve the AWS bearer fallback for OAuth.
135
135
  kiro: () => {
136
- const bearerToken = $credentialEnv("AWS_BEARER_TOKEN_KIRO");
137
- if (bearerToken) return bearerToken;
136
+ const apiKey = $credentialEnv("KIRO_API_KEY");
137
+ return apiKey?.trim().startsWith("ksk_") && !/[\x00-\x1f\x7f]/.test(apiKey)
138
+ ? apiKey
139
+ : $credentialEnv("AWS_BEARER_TOKEN_KIRO");
138
140
  },
139
141
  // GitHub Copilot uses GitHub personal access token
140
142
  "github-copilot": () => $pickCredentialEnv("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"),
package/src/types.d.ts CHANGED
@@ -399,31 +399,22 @@ export interface ToolCall {
399
399
  */
400
400
  incompleteArgumentsReason?: "truncated" | "malformed" | "conflicting" | "ambiguous";
401
401
  /**
402
- * Set when the raw argument JSON spelled a printable character as a `\uXXXX`
403
- * escape instead of a literal character. This includes ASCII landings because
404
- * a one-nibble mutation can move an intended non-ASCII scalar below U+0080.
405
- * Such a payload parses cleanly but
406
- * is unverifiable: one mistyped hex digit decodes to a different, equally
407
- * valid character, so the text can be silently wrong with no in-band evidence.
408
- * The agent loop resamples the turn a bounded number of times and then
409
- * rejects the call instead of executing it. The single bounded exception
410
- * is a tool that enumerated its display-only fields
411
- * (`displaySafeEscapedArgFields`): when every escaped scalar corroborates
412
- * a decoded non-ASCII character inside those fields, the call executes
413
- * with a warning instead — rendered question text, never executable
414
- * content, ids, or durable metadata.
415
- * Escapes that are required (control characters) or unavoidable (lone
416
- * surrogates) never set this.
402
+ * Set by current producers when raw argument JSON carries unsafe Unicode
403
+ * data, such as malformed escape evidence or a decoded unpaired surrogate.
404
+ * Valid JSON `\uXXXX` escapes are canonical spellings of the decoded string
405
+ * and current producers do not set this flag for them.
406
+ *
407
+ * Legacy producers may still set the flag for any escaped non-ASCII spelling.
408
+ * The agent loop keeps its bounded legacy resample/display-safe behavior for
409
+ * those calls while consuming the transient evidence below.
417
410
  */
418
411
  escapedNonAsciiArguments?: boolean;
419
412
  /**
420
- * Bounded, payload-free evidence for the original raw escape positions and
421
- * process-keyed scalar/path identities. Required for the display-safe terminal exemption: decoded values
422
- * alone cannot prove that an ASCII landing such as `\u0077` was not a
423
- * one-nibble mutation of a non-ASCII escape. Presence of this evidence implies
424
- * the guarded state even if a legacy producer omitted
425
- * `escapedNonAsciiArguments`. The agent consumes and removes this transient
426
- * field before the tool-call message can become durable.
413
+ * Bounded, payload-free evidence for raw Unicode argument data. Current
414
+ * producers attach it only for unsafe data; legacy producers may attach
415
+ * non-malformed positional evidence used by the display-safe compatibility
416
+ * path. The agent consumes and removes this transient field before the
417
+ * tool-call message can become durable.
427
418
  */
428
419
  escapedUnicodeArgumentEvidence?: UnicodeEscapeEvidence;
429
420
  }
@@ -630,7 +621,7 @@ export type TSchema = ZodType | TJsonSchema;
630
621
  export type Static<S> = S extends ZodType ? z.infer<S> : S extends {
631
622
  static: infer T;
632
623
  } ? T : unknown;
633
- export type RawArgumentRejectionCode = "ask-intent-review-requires-positive-round" | "ask-intent-contract-requires-non-empty-authority" | "ask-deep-interview-metadata-requires-deep-interview-gate" | "ask-round-zero-metadata-requires-full-topology-fields" | "todo-write-unknown-root-key" | "todo-write-unknown-op-entry-key" | "todo-write-unknown-op-value" | "todo-write-done-drop-requires-target" | "todo-write-unknown-init-entry-key";
624
+ export type RawArgumentRejectionCode = "ask-deep-interview-question-body-required" | "ask-intent-review-requires-positive-round" | "ask-intent-contract-requires-non-empty-authority" | "ask-deep-interview-metadata-requires-deep-interview-gate" | "ask-round-zero-metadata-requires-full-topology-fields" | "todo-write-unknown-root-key" | "todo-write-unknown-op-entry-key" | "todo-write-unknown-op-value" | "todo-write-done-drop-requires-target" | "todo-write-unknown-init-entry-key";
634
625
  /**
635
626
  * Optional structured detail attached to a raw-argument rejection. The fixed
636
627
  * per-code guidance in `RAW_ARGUMENT_REJECTION_MESSAGES` explains the shape;
package/src/types.ts CHANGED
@@ -620,31 +620,22 @@ export interface ToolCall {
620
620
  */
621
621
  incompleteArgumentsReason?: "truncated" | "malformed" | "conflicting" | "ambiguous";
622
622
  /**
623
- * Set when the raw argument JSON spelled a printable character as a `\uXXXX`
624
- * escape instead of a literal character. This includes ASCII landings because
625
- * a one-nibble mutation can move an intended non-ASCII scalar below U+0080.
626
- * Such a payload parses cleanly but
627
- * is unverifiable: one mistyped hex digit decodes to a different, equally
628
- * valid character, so the text can be silently wrong with no in-band evidence.
629
- * The agent loop resamples the turn a bounded number of times and then
630
- * rejects the call instead of executing it. The single bounded exception
631
- * is a tool that enumerated its display-only fields
632
- * (`displaySafeEscapedArgFields`): when every escaped scalar corroborates
633
- * a decoded non-ASCII character inside those fields, the call executes
634
- * with a warning instead — rendered question text, never executable
635
- * content, ids, or durable metadata.
636
- * Escapes that are required (control characters) or unavoidable (lone
637
- * surrogates) never set this.
623
+ * Set by current producers when raw argument JSON carries unsafe Unicode
624
+ * data, such as malformed escape evidence or a decoded unpaired surrogate.
625
+ * Valid JSON `\uXXXX` escapes are canonical spellings of the decoded string
626
+ * and current producers do not set this flag for them.
627
+ *
628
+ * Legacy producers may still set the flag for any escaped non-ASCII spelling.
629
+ * The agent loop keeps its bounded legacy resample/display-safe behavior for
630
+ * those calls while consuming the transient evidence below.
638
631
  */
639
632
  escapedNonAsciiArguments?: boolean;
640
633
  /**
641
- * Bounded, payload-free evidence for the original raw escape positions and
642
- * process-keyed scalar/path identities. Required for the display-safe terminal exemption: decoded values
643
- * alone cannot prove that an ASCII landing such as `\u0077` was not a
644
- * one-nibble mutation of a non-ASCII escape. Presence of this evidence implies
645
- * the guarded state even if a legacy producer omitted
646
- * `escapedNonAsciiArguments`. The agent consumes and removes this transient
647
- * field before the tool-call message can become durable.
634
+ * Bounded, payload-free evidence for raw Unicode argument data. Current
635
+ * producers attach it only for unsafe data; legacy producers may attach
636
+ * non-malformed positional evidence used by the display-safe compatibility
637
+ * path. The agent consumes and removes this transient field before the
638
+ * tool-call message can become durable.
648
639
  */
649
640
  escapedUnicodeArgumentEvidence?: UnicodeEscapeEvidence;
650
641
  }
@@ -871,6 +862,7 @@ export type TSchema = ZodType | TJsonSchema;
871
862
  export type Static<S> = S extends ZodType ? z.infer<S> : S extends { static: infer T } ? T : unknown;
872
863
 
873
864
  export type RawArgumentRejectionCode =
865
+ | "ask-deep-interview-question-body-required"
874
866
  | "ask-intent-review-requires-positive-round"
875
867
  | "ask-intent-contract-requires-non-empty-authority"
876
868
  | "ask-deep-interview-metadata-requires-deep-interview-gate"
@@ -23,6 +23,13 @@ export interface FallbackTrigger {
23
23
  export declare const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
24
24
  /** Stable code for a nominally successful response with no content or token usage. */
25
25
  export declare const EMPTY_RESPONSE_PROVIDER_CODE = "empty_response";
26
+ /**
27
+ * OpenAI's typed capacity-overload code. It arrives without an HTTP status —
28
+ * inside an HTTP 200 terminal Responses envelope or a Codex error event — so the
29
+ * code itself is the only structured evidence of the failure and must survive
30
+ * the existence gate below. It is always compared case-sensitively.
31
+ */
32
+ export declare const SERVER_OVERLOADED_PROVIDER_CODE = "server_is_overloaded";
26
33
  export type TransportHeaders = Headers | Record<string, string | undefined>;
27
34
  /**
28
35
  * Structured facts from an upstream HTTP or transport failure. Retry decisions
@@ -26,6 +26,13 @@ export interface FallbackTrigger {
26
26
  export const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
27
27
  /** Stable code for a nominally successful response with no content or token usage. */
28
28
  export const EMPTY_RESPONSE_PROVIDER_CODE = "empty_response";
29
+ /**
30
+ * OpenAI's typed capacity-overload code. It arrives without an HTTP status —
31
+ * inside an HTTP 200 terminal Responses envelope or a Codex error event — so the
32
+ * code itself is the only structured evidence of the failure and must survive
33
+ * the existence gate below. It is always compared case-sensitively.
34
+ */
35
+ export const SERVER_OVERLOADED_PROVIDER_CODE = "server_is_overloaded";
29
36
 
30
37
  export type TransportHeaders = Headers | Record<string, string | undefined>;
31
38
 
@@ -241,6 +248,11 @@ export function transportFailureFacts(
241
248
  !isContextOverflowCode(normalizedCode) &&
242
249
  normalizedCode !== STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE &&
243
250
  normalizedCode !== EMPTY_RESPONSE_PROVIDER_CODE &&
251
+ // Matched case-sensitively against the raw code: this is the one gate a
252
+ // provider-owned code can open on its own, so a cased or padded variant
253
+ // must not materialize facts that would disqualify an unrelated
254
+ // bare-default retry.
255
+ providerCode !== SERVER_OVERLOADED_PROVIDER_CODE &&
244
256
  requestBytes === undefined &&
245
257
  firstEventElapsedMs === undefined &&
246
258
  firstEventTimeoutMs === undefined &&
@@ -360,12 +372,17 @@ export function classifyFallbackTrigger(
360
372
  const retryAfterMs =
361
373
  parseRetryAfterMilliseconds(headers?.get("retry-after-ms") ?? null) ??
362
374
  parseRetryAfterSeconds(headers?.get("retry-after") ?? null);
363
- const codes = [facts.openaiErrorCode, facts.anthropicErrorType, facts.providerCode].map(value =>
364
- value?.toLowerCase(),
365
- );
375
+ const rawCodes = [facts.openaiErrorCode, facts.anthropicErrorType, facts.providerCode];
376
+ const codes = rawCodes.map(value => value?.toLowerCase());
366
377
  const code = codes[0] ?? codes[1] ?? codes[2];
378
+ // The capacity-overload code is provider-owned and is the only code that can
379
+ // classify without a status, so it is matched case-sensitively — the same
380
+ // exactness the parser and the session admission use.
381
+ const rawCode = rawCodes[0] ?? rawCodes[1] ?? rawCodes[2];
367
382
  const triggerClass: FallbackTriggerClass =
368
- code === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE || code === EMPTY_RESPONSE_PROVIDER_CODE
383
+ code === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE ||
384
+ code === EMPTY_RESPONSE_PROVIDER_CODE ||
385
+ (facts.status === undefined && rawCode === SERVER_OVERLOADED_PROVIDER_CODE)
369
386
  ? "server"
370
387
  : isQuotaCode(code)
371
388
  ? "quota"
@@ -5,6 +5,11 @@ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000;
5
5
  const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 100_000;
6
6
  const ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MS = 600_000;
7
7
  const KIMI_CODE_FIRST_EVENT_TIMEOUT_MS = 300_000;
8
+ // Local LM Studio models can spend several minutes loading weights and filling
9
+ // the prompt before emitting their first SSE event. Keep the shared default
10
+ // for other OpenAI-compatible providers, but avoid aborting legitimate local
11
+ // inference during that startup window.
12
+ const LM_STUDIO_FIRST_EVENT_TIMEOUT_MS = 300_000;
8
13
  // Ollama Cloud is a hosted proxy where queueing/cold-start plus long prefill
9
14
  // (thinking-mode requests over 1M-context sessions) routinely exceeds 120s before
10
15
  // the first streamed token; 300s matches kimi-code's floor for long-reasoning silence.
@@ -34,6 +39,7 @@ export function isGrokModelId(modelId: string | undefined): boolean {
34
39
 
35
40
  export function getProviderFirstEventTimeoutFallbackMs(provider: string): number | undefined {
36
41
  if (provider === "alibaba-token-plan") return ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MS;
42
+ if (provider === "lm-studio") return LM_STUDIO_FIRST_EVENT_TIMEOUT_MS;
37
43
  if (provider === "ollama-cloud") return OLLAMA_CLOUD_FIRST_EVENT_TIMEOUT_MS;
38
44
  return provider === "kimi-code" ? KIMI_CODE_FIRST_EVENT_TIMEOUT_MS : undefined;
39
45
  }
@@ -67,9 +67,11 @@ export declare function collectUnicodeEscapeEvidence(json: string): UnicodeEscap
67
67
  /**
68
68
  * Return evidence only when decoded tool arguments are unsafe to execute.
69
69
  *
70
- * Valid JSON escapes and literal UTF-8 have the same canonical decoded value.
71
- * Malformed JSON, duplicate/deep evidence, and unpaired UTF-16 surrogates keep
72
- * the existing fail-closed path.
70
+ * Valid JSON escapes and literal UTF-8 have the same canonical decoded value,
71
+ * including a valid scalar whose hex digits differ from what a caller intended:
72
+ * runtime syntax validation cannot infer author intent after decoding.
73
+ * Malformed escape-bearing JSON, duplicate/deep suspicious escape evidence, and
74
+ * unpaired UTF-16 surrogates keep the fail-closed path.
73
75
  */
74
76
  export declare function collectUnsafeUnicodeEscapeEvidence(json: string): UnicodeEscapeEvidence | undefined;
75
77
  /** Attach unsafe raw evidence while preserving the existing call-level guard flag. */
@@ -486,15 +486,13 @@ export function collectUnicodeEscapeEvidence(json: string): UnicodeEscapeEvidenc
486
486
 
487
487
  function hasUnpairedUnicodeSurrogate(value: unknown): boolean {
488
488
  const pending: unknown[] = [value];
489
- const seen = new WeakSet<object>();
490
489
  while (pending.length > 0) {
491
490
  const current = pending.pop();
492
491
  if (typeof current === "string") {
493
492
  if (!current.isWellFormed()) return true;
494
493
  continue;
495
494
  }
496
- if (typeof current !== "object" || current === null || seen.has(current)) continue;
497
- seen.add(current);
495
+ if (typeof current !== "object" || current === null) continue;
498
496
  if (Array.isArray(current)) {
499
497
  for (const child of current) pending.push(child);
500
498
  continue;
@@ -510,9 +508,11 @@ function hasUnpairedUnicodeSurrogate(value: unknown): boolean {
510
508
  /**
511
509
  * Return evidence only when decoded tool arguments are unsafe to execute.
512
510
  *
513
- * Valid JSON escapes and literal UTF-8 have the same canonical decoded value.
514
- * Malformed JSON, duplicate/deep evidence, and unpaired UTF-16 surrogates keep
515
- * the existing fail-closed path.
511
+ * Valid JSON escapes and literal UTF-8 have the same canonical decoded value,
512
+ * including a valid scalar whose hex digits differ from what a caller intended:
513
+ * runtime syntax validation cannot infer author intent after decoding.
514
+ * Malformed escape-bearing JSON, duplicate/deep suspicious escape evidence, and
515
+ * unpaired UTF-16 surrogates keep the fail-closed path.
516
516
  */
517
517
  export function collectUnsafeUnicodeEscapeEvidence(json: string): UnicodeEscapeEvidence | undefined {
518
518
  const hasUnicodeEscape = json.includes("\\u");
@@ -22,10 +22,10 @@ export interface HealedToolCall {
22
22
  readonly name: string;
23
23
  readonly arguments: string;
24
24
  /**
25
- * Whether the raw leaked payload spelled a printable non-ASCII character as a
26
- * `\uXXXX` escape. Captured BEFORE the normalizing round-trip below, which
27
- * decodes escapes into literal characters and would otherwise erase the only
28
- * evidence that the text is unverifiable.
25
+ * Whether the raw leaked payload carried unsafe Unicode argument data.
26
+ * Captured before the normalizing round-trip below, which would otherwise
27
+ * erase malformed escape evidence. Valid `\uXXXX` spellings decode
28
+ * canonically and leave this false.
29
29
  */
30
30
  readonly escapedNonAsciiArguments: boolean;
31
31
  readonly escapedUnicodeArgumentEvidence?: UnicodeEscapeEvidence;
@@ -40,10 +40,10 @@ export interface HealedToolCall {
40
40
  readonly name: string;
41
41
  readonly arguments: string;
42
42
  /**
43
- * Whether the raw leaked payload spelled a printable non-ASCII character as a
44
- * `\uXXXX` escape. Captured BEFORE the normalizing round-trip below, which
45
- * decodes escapes into literal characters and would otherwise erase the only
46
- * evidence that the text is unverifiable.
43
+ * Whether the raw leaked payload carried unsafe Unicode argument data.
44
+ * Captured before the normalizing round-trip below, which would otherwise
45
+ * erase malformed escape evidence. Valid `\uXXXX` spellings decode
46
+ * canonically and leave this false.
47
47
  */
48
48
  readonly escapedNonAsciiArguments: boolean;
49
49
  readonly escapedUnicodeArgumentEvidence?: UnicodeEscapeEvidence;
@@ -959,6 +959,8 @@ export function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["a
959
959
  }
960
960
 
961
961
  const RAW_ARGUMENT_REJECTION_MESSAGES: Record<RawArgumentRejectionCode, string> = {
962
+ "ask-deep-interview-question-body-required":
963
+ "deep-interview question bodies must contain a specific question, not a placeholder",
962
964
  "ask-intent-review-requires-positive-round":
963
965
  "deepInterview.intent_review is post-Round-0 only and requires a positive round",
964
966
  "ask-intent-contract-requires-non-empty-authority":