@fgv/ts-extras 5.1.0-37 → 5.1.0-39

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 (128) hide show
  1. package/dist/packlets/ai-assist/apiClient.js +66 -111
  2. package/dist/packlets/ai-assist/apiClient.js.map +1 -1
  3. package/dist/packlets/ai-assist/converters.js +16 -3
  4. package/dist/packlets/ai-assist/converters.js.map +1 -1
  5. package/dist/packlets/ai-assist/embeddingClient.js +5 -6
  6. package/dist/packlets/ai-assist/embeddingClient.js.map +1 -1
  7. package/dist/packlets/ai-assist/imageOptionsResolver.js +0 -19
  8. package/dist/packlets/ai-assist/imageOptionsResolver.js.map +1 -1
  9. package/dist/packlets/ai-assist/index.js +2 -2
  10. package/dist/packlets/ai-assist/index.js.map +1 -1
  11. package/dist/packlets/ai-assist/model.js +139 -7
  12. package/dist/packlets/ai-assist/model.js.map +1 -1
  13. package/dist/packlets/ai-assist/registry.js +58 -51
  14. package/dist/packlets/ai-assist/registry.js.map +1 -1
  15. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +38 -11
  16. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  17. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +187 -36
  18. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
  19. package/dist/packlets/ai-assist/streamingAdapters/common.js +13 -0
  20. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  21. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +12 -4
  22. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  23. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +6 -1
  24. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  25. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +10 -3
  26. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  27. package/dist/packlets/ai-assist/streamingAdapters/proxy.js +5 -2
  28. package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  29. package/dist/packlets/ai-assist/streamingClient.js +16 -16
  30. package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
  31. package/dist/packlets/crypto-utils/converters.js +18 -0
  32. package/dist/packlets/crypto-utils/converters.js.map +1 -1
  33. package/dist/packlets/crypto-utils/index.browser.js +2 -2
  34. package/dist/packlets/crypto-utils/index.browser.js.map +1 -1
  35. package/dist/packlets/crypto-utils/index.js +2 -2
  36. package/dist/packlets/crypto-utils/index.js.map +1 -1
  37. package/dist/packlets/crypto-utils/keystore/keyStore.js +19 -4
  38. package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
  39. package/dist/packlets/crypto-utils/keystore/model.js.map +1 -1
  40. package/dist/packlets/crypto-utils/model.js.map +1 -1
  41. package/dist/packlets/crypto-utils/spkiHelpers.js +104 -25
  42. package/dist/packlets/crypto-utils/spkiHelpers.js.map +1 -1
  43. package/dist/ts-extras.d.ts +377 -132
  44. package/dist/tsdoc-metadata.json +1 -1
  45. package/lib/packlets/ai-assist/apiClient.d.ts +12 -2
  46. package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -1
  47. package/lib/packlets/ai-assist/apiClient.js +65 -110
  48. package/lib/packlets/ai-assist/apiClient.js.map +1 -1
  49. package/lib/packlets/ai-assist/converters.d.ts +8 -2
  50. package/lib/packlets/ai-assist/converters.d.ts.map +1 -1
  51. package/lib/packlets/ai-assist/converters.js +17 -4
  52. package/lib/packlets/ai-assist/converters.js.map +1 -1
  53. package/lib/packlets/ai-assist/embeddingClient.d.ts.map +1 -1
  54. package/lib/packlets/ai-assist/embeddingClient.js +4 -5
  55. package/lib/packlets/ai-assist/embeddingClient.js.map +1 -1
  56. package/lib/packlets/ai-assist/imageOptionsResolver.d.ts +0 -8
  57. package/lib/packlets/ai-assist/imageOptionsResolver.d.ts.map +1 -1
  58. package/lib/packlets/ai-assist/imageOptionsResolver.js +0 -19
  59. package/lib/packlets/ai-assist/imageOptionsResolver.js.map +1 -1
  60. package/lib/packlets/ai-assist/index.d.ts +3 -3
  61. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  62. package/lib/packlets/ai-assist/index.js +6 -1
  63. package/lib/packlets/ai-assist/index.js.map +1 -1
  64. package/lib/packlets/ai-assist/model.d.ts +181 -98
  65. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  66. package/lib/packlets/ai-assist/model.js +143 -8
  67. package/lib/packlets/ai-assist/model.js.map +1 -1
  68. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  69. package/lib/packlets/ai-assist/registry.js +58 -51
  70. package/lib/packlets/ai-assist/registry.js.map +1 -1
  71. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +1 -1
  72. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
  73. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +37 -10
  74. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  75. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +82 -3
  76. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -1
  77. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +187 -35
  78. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -1
  79. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +24 -1
  80. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
  81. package/lib/packlets/ai-assist/streamingAdapters/common.js +14 -1
  82. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  83. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +10 -1
  84. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
  85. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +12 -4
  86. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  87. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +1 -1
  88. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
  89. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +6 -1
  90. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  91. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +1 -1
  92. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
  93. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +10 -3
  94. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  95. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -1
  96. package/lib/packlets/ai-assist/streamingAdapters/proxy.js +5 -2
  97. package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  98. package/lib/packlets/ai-assist/streamingClient.d.ts +1 -1
  99. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
  100. package/lib/packlets/ai-assist/streamingClient.js +15 -15
  101. package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
  102. package/lib/packlets/crypto-utils/converters.d.ts +9 -1
  103. package/lib/packlets/crypto-utils/converters.d.ts.map +1 -1
  104. package/lib/packlets/crypto-utils/converters.js +19 -1
  105. package/lib/packlets/crypto-utils/converters.js.map +1 -1
  106. package/lib/packlets/crypto-utils/index.browser.d.ts +1 -1
  107. package/lib/packlets/crypto-utils/index.browser.d.ts.map +1 -1
  108. package/lib/packlets/crypto-utils/index.browser.js +6 -2
  109. package/lib/packlets/crypto-utils/index.browser.js.map +1 -1
  110. package/lib/packlets/crypto-utils/index.d.ts +1 -1
  111. package/lib/packlets/crypto-utils/index.d.ts.map +1 -1
  112. package/lib/packlets/crypto-utils/index.js +6 -2
  113. package/lib/packlets/crypto-utils/index.js.map +1 -1
  114. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +9 -2
  115. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -1
  116. package/lib/packlets/crypto-utils/keystore/keyStore.js +19 -4
  117. package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
  118. package/lib/packlets/crypto-utils/keystore/model.d.ts +7 -0
  119. package/lib/packlets/crypto-utils/keystore/model.d.ts.map +1 -1
  120. package/lib/packlets/crypto-utils/keystore/model.js.map +1 -1
  121. package/lib/packlets/crypto-utils/model.d.ts +11 -1
  122. package/lib/packlets/crypto-utils/model.d.ts.map +1 -1
  123. package/lib/packlets/crypto-utils/model.js.map +1 -1
  124. package/lib/packlets/crypto-utils/spkiHelpers.d.ts +59 -2
  125. package/lib/packlets/crypto-utils/spkiHelpers.d.ts.map +1 -1
  126. package/lib/packlets/crypto-utils/spkiHelpers.js +108 -25
  127. package/lib/packlets/crypto-utils/spkiHelpers.js.map +1 -1
  128. package/package.json +7 -7
@@ -40,6 +40,15 @@ interface IToolCallResult {
40
40
  * (all block types in original stream order) and appends a user turn
41
41
  * with `tool_result` blocks for each executed tool call.
42
42
  *
43
+ * **Single source of truth for the id (id-correlation fix).** Each
44
+ * `tool_result.tool_use_id` is drawn from — and validated against — the set of
45
+ * assistant `tool_use.id`s actually present in the accumulation buffer. The id
46
+ * is NEVER derived from the tool name: a tool name can never match a `toolu_*`
47
+ * id, so a name fallback produces exactly the "malformed identifier" the
48
+ * provider rejects. If any tool result cannot be correlated to a buffered
49
+ * `tool_use` block with a non-empty id, the build fails loud (naming the tool)
50
+ * rather than emitting a malformed continuation.
51
+ *
43
52
  * **Constraint (E3):** The returned continuation does NOT include a forced
44
53
  * `tool_choice` field. When thinking is active, Anthropic rejects
45
54
  * `tool_choice: { type: 'any' }` and `tool_choice: { type: 'tool', ... }`
@@ -48,7 +57,7 @@ interface IToolCallResult {
48
57
  *
49
58
  * @internal
50
59
  */
51
- export declare function buildAnthropicContinuation(accBuffer: Map<number, IAccumulatedBlock>, toolResults: IToolCallResult[]): IAiClientToolContinuation;
60
+ export declare function buildAnthropicContinuation(accBuffer: Map<number, IAccumulatedBlock>, toolResults: IToolCallResult[], logger?: Logging.ILogger): Result<IAiClientToolContinuation>;
52
61
  /**
53
62
  * Builds the OpenAI / xAI Responses API follow-up input items for a
54
63
  * client-tool round-trip.
@@ -57,9 +66,16 @@ export declare function buildAnthropicContinuation(accBuffer: Map<number, IAccum
57
66
  * `function_call_output` items (the harness execution result), one pair
58
67
  * per executed tool call.
59
68
  *
69
+ * **Single source of truth for the id (id-correlation fix).** Each
70
+ * `function_call_output.call_id` is drawn from — and validated against — the
71
+ * set of `function_call.call_id`s in the accumulation map. It is NEVER derived
72
+ * from the tool name (the OpenAI parity of the Anthropic `tool_use_id` fix). A
73
+ * missing / empty / unmatched call id fails the build loud rather than emitting
74
+ * a continuation the provider would reject.
75
+ *
60
76
  * @internal
61
77
  */
62
- export declare function buildOpenAiContinuation(calls: Map<string, IAccumulatedFunctionCall>, toolResults: IToolCallResult[]): IAiClientToolContinuation;
78
+ export declare function buildOpenAiContinuation(calls: Map<string, IAccumulatedFunctionCall>, toolResults: IToolCallResult[], logger?: Logging.ILogger): Result<IAiClientToolContinuation>;
63
79
  /**
64
80
  * Builds the Gemini follow-up contents for a client-tool round-trip.
65
81
  *
@@ -67,9 +83,42 @@ export declare function buildOpenAiContinuation(calls: Map<string, IAccumulatedF
67
83
  * user turn with `functionResponse` parts (correlation by tool name, since
68
84
  * Gemini does not assign call IDs).
69
85
  *
86
+ * When thinking is enabled, Gemini stamps an opaque `thoughtSignature` on each
87
+ * `functionCall` part and requires it echoed back, verbatim, as a sibling of
88
+ * `functionCall` on the continuation's model turn — otherwise the follow-up is
89
+ * rejected with "Function call is missing a thought_signature in functionCall
90
+ * parts". The captured signature (see {@link IAccumulatedGeminiFunctionCall})
91
+ * is replayed here only when present; the key is omitted entirely when thinking
92
+ * was disabled. See https://ai.google.dev/gemini-api/docs/thought-signatures.
93
+ *
70
94
  * @internal
71
95
  */
72
96
  export declare function buildGeminiContinuation(calls: IAccumulatedGeminiFunctionCall[], toolResults: IToolCallResult[]): IAiClientToolContinuation;
97
+ /**
98
+ * Decision returned by an {@link IExecuteClientToolTurnParams.onBeforeToolExecute}
99
+ * gate for a single client-tool call.
100
+ *
101
+ * @remarks
102
+ * - `{ action: 'proceed' }` — run the tool's `execute` normally.
103
+ * - `{ action: 'deny', reason }` — do NOT run `execute`; a synthesized tool-result
104
+ * carrying `reason` is fed into the continuation exactly like a normal result so
105
+ * the model can react, and a `client-tool-result` event is emitted for the call.
106
+ * The turn continues.
107
+ *
108
+ * A gate that wants to signal a hard error (as opposed to a deliberate deny) should
109
+ * return `Result.fail` (or reject) from the callback. Note this is NOT the same as a
110
+ * `tool.execute` failure: an `execute` failure yields an `isError` tool-result and the
111
+ * **turn continues**, whereas a gate `fail`/reject **terminates the turn** — it fails
112
+ * `nextTurn` (and emits an inline `error` event) rather than synthesizing a deny.
113
+ *
114
+ * @public
115
+ */
116
+ export type IToolExecutionDecision = {
117
+ readonly action: 'proceed';
118
+ } | {
119
+ readonly action: 'deny';
120
+ readonly reason: string;
121
+ };
73
122
  /**
74
123
  * Parameters for {@link AiAssist.executeClientToolTurn}.
75
124
  *
@@ -112,7 +161,10 @@ export interface IExecuteClientToolTurnParams extends IChatRequest {
112
161
  * normalized-message path strips provider-native fields.
113
162
  */
114
163
  readonly continuationMessages?: ReadonlyArray<JsonObject>;
115
- /** Temperature (default: 0.7). */
164
+ /**
165
+ * Sampling temperature. Sent to the provider only when explicitly provided; omitted otherwise
166
+ * so the provider's own default applies (current-gen models reject a caller-supplied default).
167
+ */
116
168
  readonly temperature?: number;
117
169
  /** Server-side tools to include. */
118
170
  readonly tools?: ReadonlyArray<AiServerToolConfig>;
@@ -136,6 +188,22 @@ export interface IExecuteClientToolTurnParams extends IChatRequest {
136
188
  readonly resolvedThinking?: IResolvedThinkingConfig;
137
189
  /** Resolved model string (pre-resolved by the caller). When omitted, uses the descriptor's default model. */
138
190
  readonly model?: string;
191
+ /**
192
+ * Optional host gate invoked for each client-tool call **after** the model's raw
193
+ * args have been validated against the tool's `parametersSchema` and **before**
194
+ * `tool.execute(...)` runs. The full `tool` (including `tool.config.annotations`)
195
+ * and the validated `args` are passed so gate logic can key off a tool's behavior
196
+ * annotations (e.g. deny a `destructiveHint` call pending confirmation).
197
+ *
198
+ * Returning `{ action: 'proceed' }` (or omitting the callback) runs `execute`
199
+ * normally. Returning `{ action: 'deny', reason }` skips `execute` and synthesizes
200
+ * a denial tool-result carrying `reason` into the continuation (the model sees the
201
+ * denial and can react); **the turn continues**. A callback that returns `Result.fail`
202
+ * or rejects is a hard error that **terminates the turn** (fails `nextTurn` + emits an
203
+ * inline `error` event) — distinct from a `tool.execute` failure, which continues the
204
+ * turn with an `isError` result. A deny must therefore be explicit.
205
+ */
206
+ readonly onBeforeToolExecute?: (tool: IAiClientTool, args: unknown) => Promise<Result<IToolExecutionDecision>>;
139
207
  }
140
208
  /**
141
209
  * Return value of {@link AiAssist.executeClientToolTurn}.
@@ -155,6 +223,17 @@ export interface IExecuteClientToolTurnResult {
155
223
  */
156
224
  readonly nextTurn: Promise<Result<IAiClientToolTurnResult>>;
157
225
  }
226
+ /**
227
+ * True when a request would combine Gemini built-in grounding (a `web_search`
228
+ * server tool) with client (function) tools — a combination Gemini's
229
+ * `generateContent` API rejects with HTTP 400 (`INVALID_ARGUMENT`). Callers gate
230
+ * on `descriptor.apiFormat === 'gemini'` before consulting this; other providers
231
+ * accept the mix. Kept as a pure predicate so the conflict rule is unit-testable
232
+ * without a live stream.
233
+ *
234
+ * @internal
235
+ */
236
+ export declare function hasGeminiToolConflict(tools: ReadonlyArray<AiServerToolConfig> | undefined, clientTools: ReadonlyArray<IAiClientTool>): boolean;
158
237
  /**
159
238
  * Orchestrates a single client-tool streaming turn for any supported provider.
160
239
  *
@@ -1 +1 @@
1
- {"version":3,"file":"clientToolContinuationBuilder.d.ts","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"clientToolContinuationBuilder.d.ts","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.ts"],"names":[],"mappings":"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"}
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.buildAnthropicContinuation = buildAnthropicContinuation;
43
43
  exports.buildOpenAiContinuation = buildOpenAiContinuation;
44
44
  exports.buildGeminiContinuation = buildGeminiContinuation;
45
+ exports.hasGeminiToolConflict = hasGeminiToolConflict;
45
46
  exports.executeClientToolTurn = executeClientToolTurn;
46
47
  /**
47
48
  * Per-provider continuation message builders and the `executeClientToolTurn`
@@ -70,6 +71,19 @@ const gemini_1 = require("./gemini");
70
71
  // ============================================================================
71
72
  // Anthropic continuation builder
72
73
  // ============================================================================
74
+ /**
75
+ * Returns `true` when `id` is a usable correlation id — present and non-empty.
76
+ *
77
+ * `??` is NOT sufficient for this check: an empty string is a *bad* id (a
78
+ * `tool_use_id: ''` is rejected by Anthropic as a "malformed identifier") but
79
+ * `'' ?? fallback` yields `''`, silently passing the empty id through. Every id
80
+ * correlation in this module must use this predicate, never `??`.
81
+ *
82
+ * @internal
83
+ */
84
+ function isUsableId(id) {
85
+ return id !== undefined && id.length > 0;
86
+ }
73
87
  /**
74
88
  * Builds the Anthropic follow-up messages for a client-tool round-trip.
75
89
  *
@@ -77,6 +91,15 @@ const gemini_1 = require("./gemini");
77
91
  * (all block types in original stream order) and appends a user turn
78
92
  * with `tool_result` blocks for each executed tool call.
79
93
  *
94
+ * **Single source of truth for the id (id-correlation fix).** Each
95
+ * `tool_result.tool_use_id` is drawn from — and validated against — the set of
96
+ * assistant `tool_use.id`s actually present in the accumulation buffer. The id
97
+ * is NEVER derived from the tool name: a tool name can never match a `toolu_*`
98
+ * id, so a name fallback produces exactly the "malformed identifier" the
99
+ * provider rejects. If any tool result cannot be correlated to a buffered
100
+ * `tool_use` block with a non-empty id, the build fails loud (naming the tool)
101
+ * rather than emitting a malformed continuation.
102
+ *
80
103
  * **Constraint (E3):** The returned continuation does NOT include a forced
81
104
  * `tool_choice` field. When thinking is active, Anthropic rejects
82
105
  * `tool_choice: { type: 'any' }` and `tool_choice: { type: 'tool', ... }`
@@ -85,11 +108,14 @@ const gemini_1 = require("./gemini");
85
108
  *
86
109
  * @internal
87
110
  */
88
- function buildAnthropicContinuation(accBuffer, toolResults) {
111
+ function buildAnthropicContinuation(accBuffer, toolResults, logger) {
89
112
  // Reconstruct the assistant turn from the ordered accumulation buffer.
90
113
  // Sort by buffer key (SSE index) to restore original stream order.
91
114
  const sortedKeys = Array.from(accBuffer.keys()).sort((a, b) => a - b);
92
115
  const assistantContent = [];
116
+ // The set of assistant tool_use ids — the single source of truth every
117
+ // tool_result.tool_use_id must be drawn from.
118
+ const bufferedToolUseIds = new Set();
93
119
  for (const key of sortedKeys) {
94
120
  const block = accBuffer.get(key);
95
121
  /* c8 ignore next 1 - defensive: key always exists in map since we iterate its keys */
@@ -114,6 +140,12 @@ function buildAnthropicContinuation(accBuffer, toolResults) {
114
140
  }
115
141
  }
116
142
  else if (block.type === 'tool_use') {
143
+ // A buffered tool_use with an empty id can never be referenced by a valid
144
+ // tool_result; emitting it would corrupt the assistant turn. Fail loud.
145
+ if (!isUsableId(block.id)) {
146
+ return (0, ts_utils_1.fail)(`Anthropic continuation: buffered tool_use block for tool '${block.name}' has an empty id; ` +
147
+ `cannot build a valid continuation`);
148
+ }
117
149
  let parsedInput;
118
150
  try {
119
151
  /* c8 ignore next 1 - defensive: argsBuffer is JSON-parsed in the adapter before emitting client-tool-call-done */
@@ -124,6 +156,7 @@ function buildAnthropicContinuation(accBuffer, toolResults) {
124
156
  parsedInput = {};
125
157
  }
126
158
  /* c8 ignore stop */
159
+ bufferedToolUseIds.add(block.id);
127
160
  assistantContent.push({
128
161
  type: 'tool_use',
129
162
  id: block.id,
@@ -132,20 +165,36 @@ function buildAnthropicContinuation(accBuffer, toolResults) {
132
165
  });
133
166
  }
134
167
  }
135
- // Build user turn with tool_result blocks for each tool call.
136
- const userContent = toolResults.map((r) => {
137
- var _a;
168
+ // Build user turn with tool_result blocks for each tool call. Correlate each
169
+ // result to a buffered tool_use id — the SAME id that keys the assistant
170
+ // tool_use block — and fail loud on a missing / empty / mismatched id. A
171
+ // missing callId is a bug to surface, not paper over with a name fallback.
172
+ const userContent = [];
173
+ for (const r of toolResults) {
174
+ if (!isUsableId(r.callId)) {
175
+ return (0, ts_utils_1.fail)(`Anthropic continuation: tool '${r.toolName}' result has no call id (missing or empty); ` +
176
+ `cannot correlate it to an assistant tool_use block`);
177
+ }
178
+ if (!bufferedToolUseIds.has(r.callId)) {
179
+ return (0, ts_utils_1.fail)(`Anthropic continuation: tool '${r.toolName}' call id '${r.callId}' does not match any ` +
180
+ `buffered assistant tool_use block id`);
181
+ }
138
182
  const block = {
139
183
  type: 'tool_result',
140
184
  // eslint-disable-next-line @typescript-eslint/naming-convention
141
- tool_use_id: (_a = r.callId) !== null && _a !== void 0 ? _a : r.toolName,
185
+ tool_use_id: r.callId,
142
186
  content: r.result
143
187
  };
144
- if (r.isError) {
145
- return Object.assign(Object.assign({}, block), { is_error: true });
146
- }
147
- return block;
148
- });
188
+ userContent.push(r.isError ? Object.assign(Object.assign({}, block), { is_error: true }) : block);
189
+ }
190
+ // Decisive diagnostic: the tool_use.id ↔ tool_result.tool_use_id pairing for
191
+ // the outgoing continuation. A failing turn is field-confirmable by capturing
192
+ // this line (the pairing is by construction matched here — divergence fails
193
+ // loud above, before reaching this point).
194
+ if (logger) {
195
+ const pairing = toolResults.map((r) => `${r.toolName}:${r.callId}`).join(', ');
196
+ logger.detail(`ai-assist:anthropic-continuation tool_use.id↔tool_result.tool_use_id [${pairing}]`);
197
+ }
149
198
  const assistantMessage = {
150
199
  role: 'assistant',
151
200
  content: assistantContent
@@ -154,7 +203,7 @@ function buildAnthropicContinuation(accBuffer, toolResults) {
154
203
  role: 'user',
155
204
  content: userContent
156
205
  };
157
- return {
206
+ return (0, ts_utils_1.succeed)({
158
207
  messages: [assistantMessage, userMessage],
159
208
  toolCallsSummary: toolResults.map((r) => ({
160
209
  toolName: r.toolName,
@@ -163,7 +212,7 @@ function buildAnthropicContinuation(accBuffer, toolResults) {
163
212
  result: r.result,
164
213
  isError: r.isError
165
214
  }))
166
- };
215
+ });
167
216
  }
168
217
  // ============================================================================
169
218
  // OpenAI / xAI Responses API continuation builder
@@ -176,17 +225,25 @@ function buildAnthropicContinuation(accBuffer, toolResults) {
176
225
  * `function_call_output` items (the harness execution result), one pair
177
226
  * per executed tool call.
178
227
  *
228
+ * **Single source of truth for the id (id-correlation fix).** Each
229
+ * `function_call_output.call_id` is drawn from — and validated against — the
230
+ * set of `function_call.call_id`s in the accumulation map. It is NEVER derived
231
+ * from the tool name (the OpenAI parity of the Anthropic `tool_use_id` fix). A
232
+ * missing / empty / unmatched call id fails the build loud rather than emitting
233
+ * a continuation the provider would reject.
234
+ *
179
235
  * @internal
180
236
  */
181
- function buildOpenAiContinuation(calls, toolResults) {
182
- var _a;
237
+ function buildOpenAiContinuation(calls, toolResults, logger) {
183
238
  const items = [];
184
239
  // Emit function_call items for each call (model's side). Per the Responses API spec
185
240
  // (ResponseFunctionToolCall), `call_id` is the required correlation field — it must
186
241
  // match the matching function_call_output's `call_id` below. The optional `id` field
187
242
  // is the output-item id (`fc_*`) used to reference the streamed item; we omit it
188
243
  // because it is not load-bearing for input items.
244
+ const bufferedCallIds = new Set();
189
245
  for (const [callId, call] of calls) {
246
+ bufferedCallIds.add(callId);
190
247
  items.push({
191
248
  type: 'function_call',
192
249
  call_id: callId,
@@ -194,15 +251,31 @@ function buildOpenAiContinuation(calls, toolResults) {
194
251
  arguments: call.argsBuffer
195
252
  });
196
253
  }
197
- // Emit function_call_output items (harness execution results).
254
+ // Emit function_call_output items (harness execution results). Correlate each
255
+ // to a buffered function_call call_id — never the tool name — and fail loud on
256
+ // a missing / empty / mismatched id.
198
257
  for (const r of toolResults) {
258
+ if (!isUsableId(r.callId)) {
259
+ return (0, ts_utils_1.fail)(`OpenAI continuation: tool '${r.toolName}' result has no call id (missing or empty); ` +
260
+ `cannot correlate it to a function_call item`);
261
+ }
262
+ if (!bufferedCallIds.has(r.callId)) {
263
+ return (0, ts_utils_1.fail)(`OpenAI continuation: tool '${r.toolName}' call id '${r.callId}' does not match any ` +
264
+ `accumulated function_call call_id`);
265
+ }
199
266
  items.push({
200
267
  type: 'function_call_output',
201
- call_id: (_a = r.callId) !== null && _a !== void 0 ? _a : r.toolName,
268
+ call_id: r.callId,
202
269
  output: r.result
203
270
  });
204
271
  }
205
- return {
272
+ // Decisive diagnostic: the function_call.call_id ↔ function_call_output.call_id
273
+ // pairing for the outgoing continuation (matched by construction here).
274
+ if (logger) {
275
+ const pairing = toolResults.map((r) => `${r.toolName}:${r.callId}`).join(', ');
276
+ logger.detail(`ai-assist:openai-continuation function_call.call_id↔output.call_id [${pairing}]`);
277
+ }
278
+ return (0, ts_utils_1.succeed)({
206
279
  messages: items,
207
280
  toolCallsSummary: toolResults.map((r) => ({
208
281
  toolName: r.toolName,
@@ -211,7 +284,7 @@ function buildOpenAiContinuation(calls, toolResults) {
211
284
  result: r.result,
212
285
  isError: r.isError
213
286
  }))
214
- };
287
+ });
215
288
  }
216
289
  // ============================================================================
217
290
  // Gemini continuation builder
@@ -223,16 +296,23 @@ function buildOpenAiContinuation(calls, toolResults) {
223
296
  * user turn with `functionResponse` parts (correlation by tool name, since
224
297
  * Gemini does not assign call IDs).
225
298
  *
299
+ * When thinking is enabled, Gemini stamps an opaque `thoughtSignature` on each
300
+ * `functionCall` part and requires it echoed back, verbatim, as a sibling of
301
+ * `functionCall` on the continuation's model turn — otherwise the follow-up is
302
+ * rejected with "Function call is missing a thought_signature in functionCall
303
+ * parts". The captured signature (see {@link IAccumulatedGeminiFunctionCall})
304
+ * is replayed here only when present; the key is omitted entirely when thinking
305
+ * was disabled. See https://ai.google.dev/gemini-api/docs/thought-signatures.
306
+ *
226
307
  * @internal
227
308
  */
228
309
  function buildGeminiContinuation(calls, toolResults) {
229
- // Model turn: functionCall parts for each call.
230
- const modelParts = calls.map((call) => ({
231
- functionCall: {
310
+ // Model turn: functionCall parts for each call. Replay the part-level
311
+ // thoughtSignature as a sibling of functionCall only when present.
312
+ const modelParts = calls.map((call) => (Object.assign({ functionCall: {
232
313
  name: call.name,
233
314
  args: call.args
234
- }
235
- }));
315
+ } }, (call.thoughtSignature !== undefined ? { thoughtSignature: call.thoughtSignature } : {}))));
236
316
  // User turn: functionResponse parts for each executed result.
237
317
  // Correlation is by name since Gemini has no call IDs.
238
318
  const userParts = toolResults.map((r) => ({
@@ -263,6 +343,20 @@ function buildGeminiContinuation(calls, toolResults) {
263
343
  // ============================================================================
264
344
  // executeClientToolTurn
265
345
  // ============================================================================
346
+ /**
347
+ * True when a request would combine Gemini built-in grounding (a `web_search`
348
+ * server tool) with client (function) tools — a combination Gemini's
349
+ * `generateContent` API rejects with HTTP 400 (`INVALID_ARGUMENT`). Callers gate
350
+ * on `descriptor.apiFormat === 'gemini'` before consulting this; other providers
351
+ * accept the mix. Kept as a pure predicate so the conflict rule is unit-testable
352
+ * without a live stream.
353
+ *
354
+ * @internal
355
+ */
356
+ function hasGeminiToolConflict(tools, clientTools) {
357
+ var _a;
358
+ return clientTools.length > 0 && ((_a = tools === null || tools === void 0 ? void 0 : tools.some((t) => t.type === 'web_search')) !== null && _a !== void 0 ? _a : false);
359
+ }
266
360
  /**
267
361
  * Orchestrates a single client-tool streaming turn for any supported provider.
268
362
  *
@@ -285,7 +379,7 @@ function buildGeminiContinuation(calls, toolResults) {
285
379
  * @public
286
380
  */
287
381
  function executeClientToolTurn(params) {
288
- const { descriptor, apiKey, system, messages, continuationMessages, temperature, tools, clientTools, signal, logger, resolvedThinking, model, endpoint } = params;
382
+ const { descriptor, apiKey, system, messages, continuationMessages, temperature, tools, clientTools, signal, logger, resolvedThinking, model, endpoint, onBeforeToolExecute } = params;
289
383
  const splitResult = (0, chatRequestBuilders_1.splitChatRequest)(system, messages);
290
384
  if (splitResult.isFailure()) {
291
385
  return (0, ts_utils_1.fail)(splitResult.message);
@@ -308,11 +402,18 @@ function executeClientToolTurn(params) {
308
402
  // This is the fix for P1-1: client tools were never sent to the provider because
309
403
  // the adapters only received `tools` (server tools). Both must coexist per design §2.5.
310
404
  const effectiveTools = clientTools.length > 0 ? [...(tools !== null && tools !== void 0 ? tools : []), ...clientTools.map((t) => t.config)] : tools;
311
- const effectiveTemperature = temperature !== null && temperature !== void 0 ? temperature : 0.7;
312
- const resolvedModel = model !== null && model !== void 0 ? model : (0, model_1.resolveModel)(descriptor.defaultModel);
313
- if (resolvedModel.length === 0) {
314
- return (0, ts_utils_1.fail)(`provider "${descriptor.id}": no model resolved; pass model or set descriptor.defaultModel`);
405
+ // Gemini pre-flight: its generateContent API HTTP-400s (INVALID_ARGUMENT) when
406
+ // built-in grounding (`web_search`) and function calling (client tools) are
407
+ // combined in one request. Fail fast with a clear, actionable message rather
408
+ // than letting the opaque wire 400 surface. Other providers accept the mix.
409
+ if (descriptor.apiFormat === 'gemini' && hasGeminiToolConflict(tools, clientTools)) {
410
+ return (0, ts_utils_1.fail)('executeClientToolTurn: Gemini cannot combine web_search grounding with client (function) tools in the same request; send one or the other');
411
+ }
412
+ const modelResult = (0, model_1.resolveProviderModel)(descriptor, model);
413
+ if (modelResult.isFailure()) {
414
+ return (0, ts_utils_1.fail)(modelResult.message);
315
415
  }
416
+ const resolvedModel = modelResult.value;
316
417
  const baseUrlResult = (0, endpoint_1.resolveEffectiveBaseUrl)(descriptor, endpoint);
317
418
  if (baseUrlResult.isFailure()) {
318
419
  return (0, ts_utils_1.fail)(baseUrlResult.message);
@@ -332,13 +433,13 @@ function executeClientToolTurn(params) {
332
433
  const streamPromise = (() => {
333
434
  switch (descriptor.apiFormat) {
334
435
  case 'anthropic':
335
- return (0, anthropic_1.callAnthropicStream)(config, prompt, head, effectiveTemperature, effectiveTools, logger, signal, resolvedThinking, anthropicBuffer, continuationMessages);
436
+ return (0, anthropic_1.callAnthropicStream)(config, prompt, head, temperature, effectiveTools, logger, signal, resolvedThinking, anthropicBuffer, continuationMessages);
336
437
  case 'openai':
337
438
  return (0, openaiResponses_1.callOpenAiResponsesStream)(config, prompt,
338
439
  /* c8 ignore next 1 - defensive: openai path requires tools; empty array fallback unreachable in practice */
339
- effectiveTools !== null && effectiveTools !== void 0 ? effectiveTools : [], head, effectiveTemperature, logger, signal, resolvedThinking, openAiCallMap, continuationMessages);
440
+ effectiveTools !== null && effectiveTools !== void 0 ? effectiveTools : [], head, temperature, logger, signal, resolvedThinking, openAiCallMap, continuationMessages);
340
441
  case 'gemini':
341
- return (0, gemini_1.callGeminiStream)(config, prompt, head, effectiveTemperature, effectiveTools, logger, signal, resolvedThinking, geminiCalls, continuationMessages);
442
+ return (0, gemini_1.callGeminiStream)(config, prompt, head, temperature, effectiveTools, logger, signal, resolvedThinking, geminiCalls, continuationMessages);
342
443
  /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
343
444
  default: {
344
445
  const _exhaustive = descriptor.apiFormat;
@@ -424,6 +525,45 @@ function executeClientToolTurn(params) {
424
525
  toolResults.push({ toolName, callId, args, result: errMsg, isError: true });
425
526
  continue;
426
527
  }
528
+ // Host gate: run after arg-validation, before execute. A `deny` decision
529
+ // synthesizes a denial tool-result and continues the turn; a `fail`/reject
530
+ // from the gate itself is a hard error (mirrors an execute failure), never
531
+ // a silent deny.
532
+ if (onBeforeToolExecute !== undefined) {
533
+ const decisionResult = (yield __await((0, ts_utils_1.captureAsyncResult)(async () => onBeforeToolExecute(tool, validationResult.value)))).onSuccess((decision) => decision);
534
+ if (decisionResult.isFailure()) {
535
+ const errMsg = `${toolName} (callId=${callId}): ${decisionResult.message}`;
536
+ const resultEvent = {
537
+ type: 'client-tool-result',
538
+ toolName,
539
+ callId,
540
+ result: errMsg,
541
+ isError: true
542
+ };
543
+ yield yield __await(resultEvent);
544
+ // Gate-fail is a hard, turn-terminating error (unlike a non-fatal `deny`, whose
545
+ // generator continues). Emit an explicit `error` event so a consumer watching only
546
+ // `events` sees the fatal failure inline and can distinguish it from a deny — matching
547
+ // the stream-open / continuation hard-error paths.
548
+ yield yield __await({ type: 'error', message: errMsg });
549
+ toolResults.push({ toolName, callId, args, result: errMsg, isError: true });
550
+ resolveNextTurn((0, ts_utils_1.fail)(errMsg));
551
+ return yield __await(void 0);
552
+ }
553
+ if (decisionResult.value.action === 'deny') {
554
+ const denialMsg = `${toolName} (callId=${callId}): tool execution denied: ${decisionResult.value.reason}`;
555
+ const resultEvent = {
556
+ type: 'client-tool-result',
557
+ toolName,
558
+ callId,
559
+ result: denialMsg,
560
+ isError: true
561
+ };
562
+ yield yield __await(resultEvent);
563
+ toolResults.push({ toolName, callId, args, result: denialMsg, isError: true });
564
+ continue;
565
+ }
566
+ }
427
567
  const executeResult = yield __await((0, ts_utils_1.captureAsyncResult)(async () => tool.execute(validationResult.value)));
428
568
  const executionResult = executeResult.isSuccess()
429
569
  ? executeResult.value
@@ -505,16 +645,18 @@ function executeClientToolTurn(params) {
505
645
  resolveNextTurn((0, ts_utils_1.succeed)({ continuation: undefined, truncated, fullText }));
506
646
  return yield __await(void 0);
507
647
  }
508
- let continuation;
648
+ let continuationResult;
509
649
  switch (descriptor.apiFormat) {
510
650
  case 'anthropic':
511
- continuation = buildAnthropicContinuation(anthropicBuffer, toolResults);
651
+ continuationResult = buildAnthropicContinuation(anthropicBuffer, toolResults, logger);
512
652
  break;
513
653
  case 'openai':
514
- continuation = buildOpenAiContinuation(openAiCallMap, toolResults);
654
+ continuationResult = buildOpenAiContinuation(openAiCallMap, toolResults, logger);
515
655
  break;
516
656
  case 'gemini':
517
- continuation = buildGeminiContinuation(geminiCalls, toolResults);
657
+ // Gemini correlates by tool name by design (no call ids) — its builder
658
+ // cannot mis-key and stays non-fallible.
659
+ continuationResult = (0, ts_utils_1.succeed)(buildGeminiContinuation(geminiCalls, toolResults));
518
660
  break;
519
661
  /* c8 ignore next 5 - defensive coding: exhaustive switch guaranteed by TypeScript */
520
662
  default: {
@@ -523,6 +665,16 @@ function executeClientToolTurn(params) {
523
665
  return yield __await(void 0);
524
666
  }
525
667
  }
668
+ // A bad id-correlation fails loud here rather than emitting a malformed
669
+ // continuation the provider would reject as a "malformed identifier". Mirror
670
+ // the stream-open-failure path above: surface an `error` event so a consumer
671
+ // iterating `events` sees the failure inline, not only via `nextTurn`.
672
+ if (continuationResult.isFailure()) {
673
+ resolveNextTurn((0, ts_utils_1.fail)(continuationResult.message));
674
+ yield yield __await({ type: 'error', message: continuationResult.message });
675
+ return yield __await(void 0);
676
+ }
677
+ let continuation = continuationResult.value;
526
678
  // Prepend inbound continuationMessages so the returned continuation is cumulative.
527
679
  // A consumer that does `tail = outcome.continuation.messages` (replace) is then
528
680
  // correct for all N rounds — messages always contains the full wire tail from