@fgv/ts-extras 5.1.0-33 → 5.1.0-35
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.
- package/dist/packlets/ai-assist/apiClient.js +58 -112
- package/dist/packlets/ai-assist/apiClient.js.map +1 -1
- package/dist/packlets/ai-assist/chatRequestBuilders.js +131 -35
- package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/dist/packlets/ai-assist/converters.js +31 -1
- package/dist/packlets/ai-assist/converters.js.map +1 -1
- package/dist/packlets/ai-assist/embeddingClient.js +346 -0
- package/dist/packlets/ai-assist/embeddingClient.js.map +1 -0
- package/dist/packlets/ai-assist/http.js +75 -0
- package/dist/packlets/ai-assist/http.js.map +1 -0
- package/dist/packlets/ai-assist/index.js +6 -4
- package/dist/packlets/ai-assist/index.js.map +1 -1
- package/dist/packlets/ai-assist/jsonCompletion.js +6 -8
- package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -1
- package/dist/packlets/ai-assist/model.js +36 -1
- package/dist/packlets/ai-assist/model.js.map +1 -1
- package/dist/packlets/ai-assist/registry.js +77 -7
- package/dist/packlets/ai-assist/registry.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +176 -32
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +528 -0
- package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -0
- package/dist/packlets/ai-assist/streamingAdapters/common.js +95 -0
- package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js +34 -10
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +215 -15
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js +15 -8
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
- package/dist/packlets/ai-assist/streamingClient.js +29 -5
- package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js +23 -0
- package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
- package/dist/packlets/ai-assist/toolFormats.js +106 -10
- package/dist/packlets/ai-assist/toolFormats.js.map +1 -1
- package/dist/ts-extras.d.ts +682 -48
- package/lib/packlets/ai-assist/apiClient.d.ts +24 -34
- package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/apiClient.js +67 -121
- package/lib/packlets/ai-assist/apiClient.js.map +1 -1
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +82 -22
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
- package/lib/packlets/ai-assist/chatRequestBuilders.js +132 -34
- package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
- package/lib/packlets/ai-assist/converters.d.ts +9 -1
- package/lib/packlets/ai-assist/converters.d.ts.map +1 -1
- package/lib/packlets/ai-assist/converters.js +31 -1
- package/lib/packlets/ai-assist/converters.js.map +1 -1
- package/lib/packlets/ai-assist/embeddingClient.d.ts +69 -0
- package/lib/packlets/ai-assist/embeddingClient.d.ts.map +1 -0
- package/lib/packlets/ai-assist/embeddingClient.js +350 -0
- package/lib/packlets/ai-assist/embeddingClient.js.map +1 -0
- package/lib/packlets/ai-assist/http.d.ts +24 -0
- package/lib/packlets/ai-assist/http.d.ts.map +1 -0
- package/lib/packlets/ai-assist/http.js +78 -0
- package/lib/packlets/ai-assist/http.js.map +1 -0
- package/lib/packlets/ai-assist/index.d.ts +6 -4
- package/lib/packlets/ai-assist/index.d.ts.map +1 -1
- package/lib/packlets/ai-assist/index.js +11 -1
- package/lib/packlets/ai-assist/index.js.map +1 -1
- package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -1
- package/lib/packlets/ai-assist/jsonCompletion.js +6 -8
- package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -1
- package/lib/packlets/ai-assist/model.d.ts +377 -5
- package/lib/packlets/ai-assist/model.d.ts.map +1 -1
- package/lib/packlets/ai-assist/model.js +37 -2
- package/lib/packlets/ai-assist/model.js.map +1 -1
- package/lib/packlets/ai-assist/registry.d.ts +23 -1
- package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
- package/lib/packlets/ai-assist/registry.js +79 -7
- package/lib/packlets/ai-assist/registry.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +58 -5
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +175 -31
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +172 -0
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +534 -0
- package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +59 -11
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/common.js +97 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +16 -2
- package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js +34 -10
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +15 -2
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +214 -14
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js +14 -7
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.d.ts +17 -0
- package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/streamingClient.js +31 -6
- package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts +18 -2
- package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js +24 -0
- package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
- package/lib/packlets/ai-assist/toolFormats.d.ts +40 -9
- package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -1
- package/lib/packlets/ai-assist/toolFormats.js +107 -10
- package/lib/packlets/ai-assist/toolFormats.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
// Copyright (c) 2026 Erik Fortune
|
|
2
|
+
//
|
|
3
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
// in the Software without restriction, including without limitation the rights
|
|
6
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
// furnished to do so, subject to the following conditions:
|
|
9
|
+
//
|
|
10
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
// copies or substantial portions of the Software.
|
|
12
|
+
//
|
|
13
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
// SOFTWARE.
|
|
20
|
+
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
|
|
21
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
22
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
23
|
+
var m = o[Symbol.asyncIterator], i;
|
|
24
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
25
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
26
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
27
|
+
};
|
|
28
|
+
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
|
|
29
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
30
|
+
var g = generator.apply(thisArg, _arguments || []), i, q = [];
|
|
31
|
+
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
|
|
32
|
+
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
|
|
33
|
+
function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
|
|
34
|
+
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
|
|
35
|
+
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
|
|
36
|
+
function fulfill(value) { resume("next", value); }
|
|
37
|
+
function reject(value) { resume("throw", value); }
|
|
38
|
+
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Per-provider continuation message builders and the `executeClientToolTurn`
|
|
42
|
+
* helper for orchestrating a single client-tool round-trip.
|
|
43
|
+
*
|
|
44
|
+
* Each provider requires a different wire format for the follow-up request:
|
|
45
|
+
* - **Anthropic**: assistant turn reconstructed from the ordered accumulation
|
|
46
|
+
* buffer (thinking / redacted_thinking / text / tool_use in original stream
|
|
47
|
+
* order) + user turn with `tool_result` blocks. When thinking is active, the
|
|
48
|
+
* follow-up request must NOT set `tool_choice: { type: 'any' }` or
|
|
49
|
+
* `tool_choice: { type: 'tool', ... }` (E3 / §5.4 of b4-spike-findings).
|
|
50
|
+
* - **OpenAI / xAI Responses API**: `function_call` input items +
|
|
51
|
+
* `function_call_output` input items.
|
|
52
|
+
* - **Gemini**: model turn with `functionCall` parts + user turn with
|
|
53
|
+
* `functionResponse` parts (correlation by tool name).
|
|
54
|
+
*
|
|
55
|
+
* @packageDocumentation
|
|
56
|
+
*/
|
|
57
|
+
import { captureAsyncResult, fail, succeed } from '@fgv/ts-utils';
|
|
58
|
+
import { resolveModel } from '../model';
|
|
59
|
+
import { splitChatRequest } from '../chatRequestBuilders';
|
|
60
|
+
import { resolveEffectiveBaseUrl } from '../endpoint';
|
|
61
|
+
import { callAnthropicStream } from './anthropic';
|
|
62
|
+
import { callOpenAiResponsesStream } from './openaiResponses';
|
|
63
|
+
import { callGeminiStream } from './gemini';
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Anthropic continuation builder
|
|
66
|
+
// ============================================================================
|
|
67
|
+
/**
|
|
68
|
+
* Builds the Anthropic follow-up messages for a client-tool round-trip.
|
|
69
|
+
*
|
|
70
|
+
* Reconstructs the assistant turn from the ordered accumulation buffer
|
|
71
|
+
* (all block types in original stream order) and appends a user turn
|
|
72
|
+
* with `tool_result` blocks for each executed tool call.
|
|
73
|
+
*
|
|
74
|
+
* **Constraint (E3):** The returned continuation does NOT include a forced
|
|
75
|
+
* `tool_choice` field. When thinking is active, Anthropic rejects
|
|
76
|
+
* `tool_choice: { type: 'any' }` and `tool_choice: { type: 'tool', ... }`
|
|
77
|
+
* with an HTTP 400 error. Only `tool_choice: { type: 'auto' }` (the default,
|
|
78
|
+
* i.e. omitted) is compatible with extended thinking.
|
|
79
|
+
*
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
export function buildAnthropicContinuation(accBuffer, toolResults) {
|
|
83
|
+
// Reconstruct the assistant turn from the ordered accumulation buffer.
|
|
84
|
+
// Sort by buffer key (SSE index) to restore original stream order.
|
|
85
|
+
const sortedKeys = Array.from(accBuffer.keys()).sort((a, b) => a - b);
|
|
86
|
+
const assistantContent = [];
|
|
87
|
+
for (const key of sortedKeys) {
|
|
88
|
+
const block = accBuffer.get(key);
|
|
89
|
+
/* c8 ignore next 1 - defensive: key always exists in map since we iterate its keys */
|
|
90
|
+
if (!block)
|
|
91
|
+
continue;
|
|
92
|
+
if (block.type === 'thinking') {
|
|
93
|
+
assistantContent.push({
|
|
94
|
+
type: 'thinking',
|
|
95
|
+
thinking: block.thinking,
|
|
96
|
+
signature: block.signature
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else if (block.type === 'redacted_thinking') {
|
|
100
|
+
assistantContent.push({
|
|
101
|
+
type: 'redacted_thinking',
|
|
102
|
+
data: block.data
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
else if (block.type === 'text') {
|
|
106
|
+
if (block.text.length > 0) {
|
|
107
|
+
assistantContent.push({ type: 'text', text: block.text });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (block.type === 'tool_use') {
|
|
111
|
+
let parsedInput;
|
|
112
|
+
try {
|
|
113
|
+
/* c8 ignore next 1 - defensive: argsBuffer is JSON-parsed in the adapter before emitting client-tool-call-done */
|
|
114
|
+
parsedInput = JSON.parse(block.argsBuffer || '{}');
|
|
115
|
+
/* c8 ignore start - defensive: malformed argsBuffer defaults to empty object */
|
|
116
|
+
}
|
|
117
|
+
catch (_a) {
|
|
118
|
+
parsedInput = {};
|
|
119
|
+
}
|
|
120
|
+
/* c8 ignore stop */
|
|
121
|
+
assistantContent.push({
|
|
122
|
+
type: 'tool_use',
|
|
123
|
+
id: block.id,
|
|
124
|
+
name: block.name,
|
|
125
|
+
input: parsedInput
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Build user turn with tool_result blocks for each tool call.
|
|
130
|
+
const userContent = toolResults.map((r) => {
|
|
131
|
+
var _a;
|
|
132
|
+
const block = {
|
|
133
|
+
type: 'tool_result',
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
135
|
+
tool_use_id: (_a = r.callId) !== null && _a !== void 0 ? _a : r.toolName,
|
|
136
|
+
content: r.result
|
|
137
|
+
};
|
|
138
|
+
if (r.isError) {
|
|
139
|
+
return Object.assign(Object.assign({}, block), { is_error: true });
|
|
140
|
+
}
|
|
141
|
+
return block;
|
|
142
|
+
});
|
|
143
|
+
const assistantMessage = {
|
|
144
|
+
role: 'assistant',
|
|
145
|
+
content: assistantContent
|
|
146
|
+
};
|
|
147
|
+
const userMessage = {
|
|
148
|
+
role: 'user',
|
|
149
|
+
content: userContent
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
messages: [assistantMessage, userMessage],
|
|
153
|
+
toolCallsSummary: toolResults.map((r) => ({
|
|
154
|
+
toolName: r.toolName,
|
|
155
|
+
callId: r.callId,
|
|
156
|
+
args: r.args,
|
|
157
|
+
result: r.result,
|
|
158
|
+
isError: r.isError
|
|
159
|
+
}))
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// OpenAI / xAI Responses API continuation builder
|
|
164
|
+
// ============================================================================
|
|
165
|
+
/**
|
|
166
|
+
* Builds the OpenAI / xAI Responses API follow-up input items for a
|
|
167
|
+
* client-tool round-trip.
|
|
168
|
+
*
|
|
169
|
+
* Emits `function_call` items (the model's call) followed by
|
|
170
|
+
* `function_call_output` items (the harness execution result), one pair
|
|
171
|
+
* per executed tool call.
|
|
172
|
+
*
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
export function buildOpenAiContinuation(calls, toolResults) {
|
|
176
|
+
var _a;
|
|
177
|
+
const items = [];
|
|
178
|
+
// Emit function_call items for each call (model's side). Per the Responses API spec
|
|
179
|
+
// (ResponseFunctionToolCall), `call_id` is the required correlation field — it must
|
|
180
|
+
// match the matching function_call_output's `call_id` below. The optional `id` field
|
|
181
|
+
// is the output-item id (`fc_*`) used to reference the streamed item; we omit it
|
|
182
|
+
// because it is not load-bearing for input items.
|
|
183
|
+
for (const [callId, call] of calls) {
|
|
184
|
+
items.push({
|
|
185
|
+
type: 'function_call',
|
|
186
|
+
call_id: callId,
|
|
187
|
+
name: call.name,
|
|
188
|
+
arguments: call.argsBuffer
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// Emit function_call_output items (harness execution results).
|
|
192
|
+
for (const r of toolResults) {
|
|
193
|
+
items.push({
|
|
194
|
+
type: 'function_call_output',
|
|
195
|
+
call_id: (_a = r.callId) !== null && _a !== void 0 ? _a : r.toolName,
|
|
196
|
+
output: r.result
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
messages: items,
|
|
201
|
+
toolCallsSummary: toolResults.map((r) => ({
|
|
202
|
+
toolName: r.toolName,
|
|
203
|
+
callId: r.callId,
|
|
204
|
+
args: r.args,
|
|
205
|
+
result: r.result,
|
|
206
|
+
isError: r.isError
|
|
207
|
+
}))
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Gemini continuation builder
|
|
212
|
+
// ============================================================================
|
|
213
|
+
/**
|
|
214
|
+
* Builds the Gemini follow-up contents for a client-tool round-trip.
|
|
215
|
+
*
|
|
216
|
+
* Emits a model turn with `functionCall` parts (one per tool call) and a
|
|
217
|
+
* user turn with `functionResponse` parts (correlation by tool name, since
|
|
218
|
+
* Gemini does not assign call IDs).
|
|
219
|
+
*
|
|
220
|
+
* @internal
|
|
221
|
+
*/
|
|
222
|
+
export function buildGeminiContinuation(calls, toolResults) {
|
|
223
|
+
// Model turn: functionCall parts for each call.
|
|
224
|
+
const modelParts = calls.map((call) => ({
|
|
225
|
+
functionCall: {
|
|
226
|
+
name: call.name,
|
|
227
|
+
args: call.args
|
|
228
|
+
}
|
|
229
|
+
}));
|
|
230
|
+
// User turn: functionResponse parts for each executed result.
|
|
231
|
+
// Correlation is by name since Gemini has no call IDs.
|
|
232
|
+
const userParts = toolResults.map((r) => ({
|
|
233
|
+
functionResponse: {
|
|
234
|
+
name: r.toolName,
|
|
235
|
+
response: Object.assign({ content: r.result }, (r.isError ? { error: true } : {}))
|
|
236
|
+
}
|
|
237
|
+
}));
|
|
238
|
+
const modelMessage = {
|
|
239
|
+
role: 'model',
|
|
240
|
+
parts: modelParts
|
|
241
|
+
};
|
|
242
|
+
const userMessage = {
|
|
243
|
+
role: 'user',
|
|
244
|
+
parts: userParts
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
messages: [modelMessage, userMessage],
|
|
248
|
+
toolCallsSummary: toolResults.map((r) => ({
|
|
249
|
+
toolName: r.toolName,
|
|
250
|
+
callId: r.callId,
|
|
251
|
+
args: r.args,
|
|
252
|
+
result: r.result,
|
|
253
|
+
isError: r.isError
|
|
254
|
+
}))
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// executeClientToolTurn
|
|
259
|
+
// ============================================================================
|
|
260
|
+
/**
|
|
261
|
+
* Orchestrates a single client-tool streaming turn for any supported provider.
|
|
262
|
+
*
|
|
263
|
+
* Starts a streaming request, iterates the underlying provider stream, and:
|
|
264
|
+
* - Forwards `text-delta`, `tool-event`, `client-tool-call-start`, and
|
|
265
|
+
* `client-tool-call-done` events through to the consumer.
|
|
266
|
+
* - For each `client-tool-call-done` event: validates the raw args against the
|
|
267
|
+
* tool's `parametersSchema`, invokes `execute(typedArgs)`, and emits a
|
|
268
|
+
* `client-tool-result` event.
|
|
269
|
+
* - After stream completion: builds the per-provider continuation (or
|
|
270
|
+
* `{ continuation: undefined }` when no tool calls occurred) and resolves
|
|
271
|
+
* `nextTurn`.
|
|
272
|
+
*
|
|
273
|
+
* **Anthropic constraint (E3):** The continuation for Anthropic does not set
|
|
274
|
+
* a forced `tool_choice`. Only `tool_choice: 'auto'` (the default, i.e.
|
|
275
|
+
* omitted) is compatible with extended thinking.
|
|
276
|
+
*
|
|
277
|
+
* @param params - Turn parameters
|
|
278
|
+
* @returns `{ events, nextTurn }` — stream iterable + completion promise
|
|
279
|
+
* @public
|
|
280
|
+
*/
|
|
281
|
+
export function executeClientToolTurn(params) {
|
|
282
|
+
const { descriptor, apiKey, system, messages, continuationMessages, temperature, tools, clientTools, signal, logger, resolvedThinking, model, endpoint } = params;
|
|
283
|
+
const splitResult = splitChatRequest(system, messages);
|
|
284
|
+
if (splitResult.isFailure()) {
|
|
285
|
+
return fail(splitResult.message);
|
|
286
|
+
}
|
|
287
|
+
const { prompt, head } = splitResult.value;
|
|
288
|
+
if (prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {
|
|
289
|
+
return fail(`provider "${descriptor.id}" does not accept image input`);
|
|
290
|
+
}
|
|
291
|
+
// Build a lookup map of client tools by name for fast access.
|
|
292
|
+
// Fail fast on duplicate names — silently overwriting would cause one tool
|
|
293
|
+
// to shadow another with no observable signal.
|
|
294
|
+
const toolsByName = new Map();
|
|
295
|
+
for (const tool of clientTools) {
|
|
296
|
+
if (toolsByName.has(tool.config.name)) {
|
|
297
|
+
return fail(`executeClientToolTurn: duplicate client tool name '${tool.config.name}'`);
|
|
298
|
+
}
|
|
299
|
+
toolsByName.set(tool.config.name, tool);
|
|
300
|
+
}
|
|
301
|
+
// Merge server tools and client tool configs into a single array for the provider.
|
|
302
|
+
// This is the fix for P1-1: client tools were never sent to the provider because
|
|
303
|
+
// the adapters only received `tools` (server tools). Both must coexist per design §2.5.
|
|
304
|
+
const effectiveTools = clientTools.length > 0 ? [...(tools !== null && tools !== void 0 ? tools : []), ...clientTools.map((t) => t.config)] : tools;
|
|
305
|
+
const effectiveTemperature = temperature !== null && temperature !== void 0 ? temperature : 0.7;
|
|
306
|
+
const resolvedModel = model !== null && model !== void 0 ? model : resolveModel(descriptor.defaultModel);
|
|
307
|
+
if (resolvedModel.length === 0) {
|
|
308
|
+
return fail(`provider "${descriptor.id}": no model resolved; pass model or set descriptor.defaultModel`);
|
|
309
|
+
}
|
|
310
|
+
const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);
|
|
311
|
+
if (baseUrlResult.isFailure()) {
|
|
312
|
+
return fail(baseUrlResult.message);
|
|
313
|
+
}
|
|
314
|
+
const config = {
|
|
315
|
+
baseUrl: baseUrlResult.value,
|
|
316
|
+
apiKey,
|
|
317
|
+
model: resolvedModel
|
|
318
|
+
};
|
|
319
|
+
// Accumulation buffers — populated by the adapter, read by the builder.
|
|
320
|
+
const anthropicBuffer = new Map();
|
|
321
|
+
const openAiCallMap = new Map();
|
|
322
|
+
const geminiCalls = [];
|
|
323
|
+
// Collected tool results, populated as each client-tool-call-done is processed.
|
|
324
|
+
const toolResults = [];
|
|
325
|
+
// Stream start: open the underlying adapter stream.
|
|
326
|
+
const streamPromise = (() => {
|
|
327
|
+
switch (descriptor.apiFormat) {
|
|
328
|
+
case 'anthropic':
|
|
329
|
+
return callAnthropicStream(config, prompt, head, effectiveTemperature, effectiveTools, logger, signal, resolvedThinking, anthropicBuffer, continuationMessages);
|
|
330
|
+
case 'openai':
|
|
331
|
+
return callOpenAiResponsesStream(config, prompt,
|
|
332
|
+
/* c8 ignore next 1 - defensive: openai path requires tools; empty array fallback unreachable in practice */
|
|
333
|
+
effectiveTools !== null && effectiveTools !== void 0 ? effectiveTools : [], head, effectiveTemperature, logger, signal, resolvedThinking, openAiCallMap, continuationMessages);
|
|
334
|
+
case 'gemini':
|
|
335
|
+
return callGeminiStream(config, prompt, head, effectiveTemperature, effectiveTools, logger, signal, resolvedThinking, geminiCalls, continuationMessages);
|
|
336
|
+
/* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
|
|
337
|
+
default: {
|
|
338
|
+
const _exhaustive = descriptor.apiFormat;
|
|
339
|
+
return Promise.resolve(fail(`unsupported API format: ${String(_exhaustive)}`));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
})();
|
|
343
|
+
// Resolve controls for `nextTurn`.
|
|
344
|
+
let resolveNextTurn;
|
|
345
|
+
const nextTurn = new Promise((resolve) => {
|
|
346
|
+
resolveNextTurn = resolve;
|
|
347
|
+
});
|
|
348
|
+
// The unified-event generator: opens the stream, proxies events, executes tools.
|
|
349
|
+
function eventGenerator() {
|
|
350
|
+
return __asyncGenerator(this, arguments, function* eventGenerator_1() {
|
|
351
|
+
var _a, e_1, _b, _c;
|
|
352
|
+
const streamResult = yield __await(streamPromise);
|
|
353
|
+
if (streamResult.isFailure()) {
|
|
354
|
+
resolveNextTurn(fail(streamResult.message));
|
|
355
|
+
yield yield __await({ type: 'error', message: streamResult.message });
|
|
356
|
+
return yield __await(void 0);
|
|
357
|
+
}
|
|
358
|
+
let truncated = false;
|
|
359
|
+
let fullText = '';
|
|
360
|
+
let streamError;
|
|
361
|
+
try {
|
|
362
|
+
for (var _d = true, _e = __asyncValues(streamResult.value), _f; _f = yield __await(_e.next()), _a = _f.done, !_a; _d = true) {
|
|
363
|
+
_c = _f.value;
|
|
364
|
+
_d = false;
|
|
365
|
+
const event = _c;
|
|
366
|
+
if (event.type === 'done') {
|
|
367
|
+
truncated = event.truncated;
|
|
368
|
+
fullText = event.fullText;
|
|
369
|
+
yield yield __await(event);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (event.type === 'error') {
|
|
373
|
+
streamError = event.message;
|
|
374
|
+
yield yield __await(event);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (event.type === 'text-delta') {
|
|
378
|
+
yield yield __await(event);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (event.type === 'tool-event') {
|
|
382
|
+
yield yield __await(event);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (event.type === 'client-tool-call-start') {
|
|
386
|
+
yield yield __await(event);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (event.type === 'client-tool-call-done') {
|
|
390
|
+
yield yield __await(event);
|
|
391
|
+
const { toolName, callId, args } = event;
|
|
392
|
+
const tool = toolsByName.get(toolName);
|
|
393
|
+
if (!tool) {
|
|
394
|
+
const errMsg = `model called unknown tool: ${toolName}`;
|
|
395
|
+
const resultEvent = {
|
|
396
|
+
type: 'client-tool-result',
|
|
397
|
+
toolName,
|
|
398
|
+
callId,
|
|
399
|
+
result: errMsg,
|
|
400
|
+
isError: true
|
|
401
|
+
};
|
|
402
|
+
yield yield __await(resultEvent);
|
|
403
|
+
toolResults.push({ toolName, callId, args, result: errMsg, isError: true });
|
|
404
|
+
resolveNextTurn(fail(errMsg));
|
|
405
|
+
return yield __await(void 0);
|
|
406
|
+
}
|
|
407
|
+
const validationResult = tool.config.parametersSchema.validate(args);
|
|
408
|
+
if (validationResult.isFailure()) {
|
|
409
|
+
const errMsg = `${toolName} (callId=${callId}): ${validationResult.message}`;
|
|
410
|
+
const resultEvent = {
|
|
411
|
+
type: 'client-tool-result',
|
|
412
|
+
toolName,
|
|
413
|
+
callId,
|
|
414
|
+
result: errMsg,
|
|
415
|
+
isError: true
|
|
416
|
+
};
|
|
417
|
+
yield yield __await(resultEvent);
|
|
418
|
+
toolResults.push({ toolName, callId, args, result: errMsg, isError: true });
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const executeResult = yield __await(captureAsyncResult(async () => tool.execute(validationResult.value)));
|
|
422
|
+
const executionResult = executeResult.isSuccess()
|
|
423
|
+
? executeResult.value
|
|
424
|
+
: executeResult;
|
|
425
|
+
if (executionResult.isFailure()) {
|
|
426
|
+
const errMsg = `${toolName} (callId=${callId}): ${executionResult.message}`;
|
|
427
|
+
const resultEvent = {
|
|
428
|
+
type: 'client-tool-result',
|
|
429
|
+
toolName,
|
|
430
|
+
callId,
|
|
431
|
+
result: errMsg,
|
|
432
|
+
isError: true
|
|
433
|
+
};
|
|
434
|
+
yield yield __await(resultEvent);
|
|
435
|
+
toolResults.push({ toolName, callId, args, result: errMsg, isError: true });
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
// JSON.stringify can throw (circular references) or return undefined
|
|
439
|
+
// (e.g. for a bare `undefined` value, or a value of type `function`).
|
|
440
|
+
// Either outcome violates the client-tool-result event contract, so
|
|
441
|
+
// emit an isError result with diagnostic context instead.
|
|
442
|
+
let resultStr;
|
|
443
|
+
try {
|
|
444
|
+
const stringified = JSON.stringify(executionResult.value);
|
|
445
|
+
if (stringified === undefined) {
|
|
446
|
+
const errMsg = `${toolName} (callId=${callId}): tool returned a non-serializable value (JSON.stringify produced undefined)`;
|
|
447
|
+
const resultEvent = {
|
|
448
|
+
type: 'client-tool-result',
|
|
449
|
+
toolName,
|
|
450
|
+
callId,
|
|
451
|
+
result: errMsg,
|
|
452
|
+
isError: true
|
|
453
|
+
};
|
|
454
|
+
yield yield __await(resultEvent);
|
|
455
|
+
toolResults.push({ toolName, callId, args, result: errMsg, isError: true });
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
resultStr = stringified;
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
const errMsg = `${toolName} (callId=${callId}): failed to serialize tool result: ${e.message}`;
|
|
462
|
+
const resultEvent = {
|
|
463
|
+
type: 'client-tool-result',
|
|
464
|
+
toolName,
|
|
465
|
+
callId,
|
|
466
|
+
result: errMsg,
|
|
467
|
+
isError: true
|
|
468
|
+
};
|
|
469
|
+
yield yield __await(resultEvent);
|
|
470
|
+
toolResults.push({ toolName, callId, args, result: errMsg, isError: true });
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const resultEvent = {
|
|
474
|
+
type: 'client-tool-result',
|
|
475
|
+
toolName,
|
|
476
|
+
callId,
|
|
477
|
+
result: resultStr,
|
|
478
|
+
isError: false
|
|
479
|
+
};
|
|
480
|
+
yield yield __await(resultEvent);
|
|
481
|
+
toolResults.push({ toolName, callId, args, result: resultStr, isError: false });
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
487
|
+
finally {
|
|
488
|
+
try {
|
|
489
|
+
if (!_d && !_a && (_b = _e.return)) yield __await(_b.call(_e));
|
|
490
|
+
}
|
|
491
|
+
finally { if (e_1) throw e_1.error; }
|
|
492
|
+
}
|
|
493
|
+
// Stream has ended. Build the continuation.
|
|
494
|
+
if (streamError !== undefined) {
|
|
495
|
+
resolveNextTurn(fail(streamError));
|
|
496
|
+
return yield __await(void 0);
|
|
497
|
+
}
|
|
498
|
+
if (toolResults.length === 0) {
|
|
499
|
+
resolveNextTurn(succeed({ continuation: undefined, truncated, fullText }));
|
|
500
|
+
return yield __await(void 0);
|
|
501
|
+
}
|
|
502
|
+
let continuation;
|
|
503
|
+
switch (descriptor.apiFormat) {
|
|
504
|
+
case 'anthropic':
|
|
505
|
+
continuation = buildAnthropicContinuation(anthropicBuffer, toolResults);
|
|
506
|
+
break;
|
|
507
|
+
case 'openai':
|
|
508
|
+
continuation = buildOpenAiContinuation(openAiCallMap, toolResults);
|
|
509
|
+
break;
|
|
510
|
+
case 'gemini':
|
|
511
|
+
continuation = buildGeminiContinuation(geminiCalls, toolResults);
|
|
512
|
+
break;
|
|
513
|
+
/* c8 ignore next 5 - defensive coding: exhaustive switch guaranteed by TypeScript */
|
|
514
|
+
default: {
|
|
515
|
+
const _exhaustive = descriptor.apiFormat;
|
|
516
|
+
resolveNextTurn(fail(`unsupported API format: ${String(_exhaustive)}`));
|
|
517
|
+
return yield __await(void 0);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
resolveNextTurn(succeed({ continuation, truncated, fullText }));
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
return succeed({
|
|
524
|
+
events: { [Symbol.asyncIterator]: () => eventGenerator() },
|
|
525
|
+
nextTurn
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
//# sourceMappingURL=clientToolContinuationBuilder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientToolContinuationBuilder.js","sourceRoot":"","sources":["../../../../src/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Per-provider continuation message builders and the `executeClientToolTurn`\n * helper for orchestrating a single client-tool round-trip.\n *\n * Each provider requires a different wire format for the follow-up request:\n * - **Anthropic**: assistant turn reconstructed from the ordered accumulation\n * buffer (thinking / redacted_thinking / text / tool_use in original stream\n * order) + user turn with `tool_result` blocks. When thinking is active, the\n * follow-up request must NOT set `tool_choice: { type: 'any' }` or\n * `tool_choice: { type: 'tool', ... }` (E3 / §5.4 of b4-spike-findings).\n * - **OpenAI / xAI Responses API**: `function_call` input items +\n * `function_call_output` input items.\n * - **Gemini**: model turn with `functionCall` parts + user turn with\n * `functionResponse` parts (correlation by tool name).\n *\n * @packageDocumentation\n */\n\nimport { captureAsyncResult, fail, type Logging, Result, succeed } from '@fgv/ts-utils';\nimport { type JsonArray, type JsonObject } from '@fgv/ts-json-base';\n\nimport {\n type AiServerToolConfig,\n type AiToolConfig,\n type IAiClientTool,\n type IAiClientToolContinuation,\n type IAiClientToolTurnResult,\n type IAiStreamEvent,\n type IChatRequest,\n type IAiProviderDescriptor,\n resolveModel\n} from '../model';\nimport { type IResolvedThinkingConfig } from '../thinkingOptionsResolver';\nimport { splitChatRequest } from '../chatRequestBuilders';\nimport { resolveEffectiveBaseUrl } from '../endpoint';\nimport { type IAccumulatedBlock } from './anthropic';\nimport { type IAccumulatedFunctionCall } from './openaiResponses';\nimport { type IAccumulatedGeminiFunctionCall } from './gemini';\nimport { callAnthropicStream } from './anthropic';\nimport { callOpenAiResponsesStream } from './openaiResponses';\nimport { callGeminiStream } from './gemini';\nimport { type IStreamApiConfig } from './common';\n\n// ============================================================================\n// Tool-result accumulation (internal)\n// ============================================================================\n\n/**\n * Accumulated result for a single tool call, collected during stream iteration.\n * @internal\n */\ninterface IToolCallResult {\n readonly toolName: string;\n readonly callId?: string;\n readonly args: JsonObject;\n readonly result: string;\n readonly isError: boolean;\n}\n\n// ============================================================================\n// Anthropic continuation builder\n// ============================================================================\n\n/**\n * Builds the Anthropic follow-up messages for a client-tool round-trip.\n *\n * Reconstructs the assistant turn from the ordered accumulation buffer\n * (all block types in original stream order) and appends a user turn\n * with `tool_result` blocks for each executed tool call.\n *\n * **Constraint (E3):** The returned continuation does NOT include a forced\n * `tool_choice` field. When thinking is active, Anthropic rejects\n * `tool_choice: { type: 'any' }` and `tool_choice: { type: 'tool', ... }`\n * with an HTTP 400 error. Only `tool_choice: { type: 'auto' }` (the default,\n * i.e. omitted) is compatible with extended thinking.\n *\n * @internal\n */\nexport function buildAnthropicContinuation(\n accBuffer: Map<number, IAccumulatedBlock>,\n toolResults: IToolCallResult[]\n): IAiClientToolContinuation {\n // Reconstruct the assistant turn from the ordered accumulation buffer.\n // Sort by buffer key (SSE index) to restore original stream order.\n const sortedKeys = Array.from(accBuffer.keys()).sort((a, b) => a - b);\n const assistantContent: JsonArray = [];\n\n for (const key of sortedKeys) {\n const block = accBuffer.get(key);\n /* c8 ignore next 1 - defensive: key always exists in map since we iterate its keys */\n if (!block) continue;\n if (block.type === 'thinking') {\n assistantContent.push({\n type: 'thinking',\n thinking: block.thinking,\n signature: block.signature\n });\n } else if (block.type === 'redacted_thinking') {\n assistantContent.push({\n type: 'redacted_thinking',\n data: block.data\n });\n } else if (block.type === 'text') {\n if (block.text.length > 0) {\n assistantContent.push({ type: 'text', text: block.text });\n }\n } else if (block.type === 'tool_use') {\n let parsedInput: JsonObject;\n try {\n /* c8 ignore next 1 - defensive: argsBuffer is JSON-parsed in the adapter before emitting client-tool-call-done */\n parsedInput = JSON.parse(block.argsBuffer || '{}') as JsonObject;\n /* c8 ignore start - defensive: malformed argsBuffer defaults to empty object */\n } catch {\n parsedInput = {};\n }\n /* c8 ignore stop */\n assistantContent.push({\n type: 'tool_use',\n id: block.id,\n name: block.name,\n input: parsedInput\n });\n }\n }\n\n // Build user turn with tool_result blocks for each tool call.\n const userContent: JsonArray = toolResults.map((r): JsonObject => {\n const block: JsonObject = {\n type: 'tool_result',\n // eslint-disable-next-line @typescript-eslint/naming-convention\n tool_use_id: r.callId ?? r.toolName,\n content: r.result\n };\n if (r.isError) {\n return { ...block, is_error: true };\n }\n return block;\n });\n\n const assistantMessage: JsonObject = {\n role: 'assistant',\n content: assistantContent\n };\n const userMessage: JsonObject = {\n role: 'user',\n content: userContent\n };\n\n return {\n messages: [assistantMessage, userMessage],\n toolCallsSummary: toolResults.map((r) => ({\n toolName: r.toolName,\n callId: r.callId,\n args: r.args,\n result: r.result,\n isError: r.isError\n }))\n };\n}\n\n// ============================================================================\n// OpenAI / xAI Responses API continuation builder\n// ============================================================================\n\n/**\n * Builds the OpenAI / xAI Responses API follow-up input items for a\n * client-tool round-trip.\n *\n * Emits `function_call` items (the model's call) followed by\n * `function_call_output` items (the harness execution result), one pair\n * per executed tool call.\n *\n * @internal\n */\nexport function buildOpenAiContinuation(\n calls: Map<string, IAccumulatedFunctionCall>,\n toolResults: IToolCallResult[]\n): IAiClientToolContinuation {\n const items: JsonObject[] = [];\n\n // Emit function_call items for each call (model's side). Per the Responses API spec\n // (ResponseFunctionToolCall), `call_id` is the required correlation field — it must\n // match the matching function_call_output's `call_id` below. The optional `id` field\n // is the output-item id (`fc_*`) used to reference the streamed item; we omit it\n // because it is not load-bearing for input items.\n for (const [callId, call] of calls) {\n items.push({\n type: 'function_call',\n call_id: callId,\n name: call.name,\n arguments: call.argsBuffer\n });\n }\n\n // Emit function_call_output items (harness execution results).\n for (const r of toolResults) {\n items.push({\n type: 'function_call_output',\n call_id: r.callId ?? r.toolName,\n output: r.result\n });\n }\n\n return {\n messages: items,\n toolCallsSummary: toolResults.map((r) => ({\n toolName: r.toolName,\n callId: r.callId,\n args: r.args,\n result: r.result,\n isError: r.isError\n }))\n };\n}\n\n// ============================================================================\n// Gemini continuation builder\n// ============================================================================\n\n/**\n * Builds the Gemini follow-up contents for a client-tool round-trip.\n *\n * Emits a model turn with `functionCall` parts (one per tool call) and a\n * user turn with `functionResponse` parts (correlation by tool name, since\n * Gemini does not assign call IDs).\n *\n * @internal\n */\nexport function buildGeminiContinuation(\n calls: IAccumulatedGeminiFunctionCall[],\n toolResults: IToolCallResult[]\n): IAiClientToolContinuation {\n // Model turn: functionCall parts for each call.\n const modelParts: JsonArray = calls.map(\n (call): JsonObject => ({\n functionCall: {\n name: call.name,\n args: call.args\n }\n })\n );\n\n // User turn: functionResponse parts for each executed result.\n // Correlation is by name since Gemini has no call IDs.\n const userParts: JsonArray = toolResults.map(\n (r): JsonObject => ({\n functionResponse: {\n name: r.toolName,\n response: {\n content: r.result,\n ...(r.isError ? { error: true } : {})\n }\n }\n })\n );\n\n const modelMessage: JsonObject = {\n role: 'model',\n parts: modelParts\n };\n const userMessage: JsonObject = {\n role: 'user',\n parts: userParts\n };\n\n return {\n messages: [modelMessage, userMessage],\n toolCallsSummary: toolResults.map((r) => ({\n toolName: r.toolName,\n callId: r.callId,\n args: r.args,\n result: r.result,\n isError: r.isError\n }))\n };\n}\n\n// ============================================================================\n// executeClientToolTurn parameters\n// ============================================================================\n\n/**\n * Parameters for {@link AiAssist.executeClientToolTurn}.\n *\n * @remarks\n * Carries the unified {@link AiAssist.IChatRequest} shape (`system?` + ordered\n * `messages`): the last message is the current user turn and the preceding\n * messages are history, linearized before the current turn — identically to the\n * completion and streaming paths. {@link IExecuteClientToolTurnParams.continuationMessages}\n * remains a distinct post-current-turn axis (see below).\n *\n * @public\n */\nexport interface IExecuteClientToolTurnParams extends IChatRequest {\n /** The provider descriptor for routing (Anthropic / OpenAI / Gemini). */\n readonly descriptor: IAiProviderDescriptor;\n /** API key for authentication. */\n readonly apiKey: string;\n /**\n * Provider-specific continuation messages to append after the current user\n * message. Used to supply the output of {@link AiAssist.IAiClientToolContinuation}'s\n * `messages` field from a prior turn back to the provider in the follow-up request.\n *\n * Each provider applies its own shape guard to the supplied wire objects:\n * - Anthropic: projects each entry to `{ role, content }` (sufficient for\n * thinking blocks and `tool_result` arrays).\n * - OpenAI / xAI Responses: passes each item verbatim (`function_call` /\n * `function_call_output` items carry distinct fields per `type`); only guards\n * that each entry is a JSON object.\n * - Gemini: projects each entry to `{ role, parts }`.\n *\n * Entries that fail their provider's shape check are silently skipped.\n */\n readonly continuationMessages?: ReadonlyArray<JsonObject>;\n /** Temperature (default: 0.7). */\n readonly temperature?: number;\n /** Server-side tools to include. */\n readonly tools?: ReadonlyArray<AiServerToolConfig>;\n /** Client-defined tools available for the model to call. */\n readonly clientTools: ReadonlyArray<IAiClientTool>;\n /** Optional abort signal. */\n readonly signal?: AbortSignal;\n /**\n * Optional override of the descriptor's default base URL. Same semantics as\n * the non-streaming completion path and `callProviderCompletionStream`: a\n * well-formed `http`/`https` URL is substituted for `descriptor.baseUrl`\n * when composing the per-format request, with the per-format suffix appended\n * unchanged. Validated at the dispatcher; auth shape is unaffected. Use this\n * to point a client-tool turn at a local / LAN OpenAI-compatible server\n * (Ollama, LM Studio, llama.cpp).\n */\n readonly endpoint?: string;\n /** Optional logger for diagnostics. */\n readonly logger?: Logging.ILogger;\n /** Optional resolved thinking config (pre-resolved by the caller). */\n readonly resolvedThinking?: IResolvedThinkingConfig;\n /** Resolved model string (pre-resolved by the caller). When omitted, uses the descriptor's default model. */\n readonly model?: string;\n}\n\n/**\n * Return value of {@link AiAssist.executeClientToolTurn}.\n * @public\n */\nexport interface IExecuteClientToolTurnResult {\n /**\n * The unified-event iterable. Callers iterate this to drive the streaming UI.\n * The iterable forwards `text-delta`, `tool-event`, `client-tool-call-start`,\n * `client-tool-call-done`, and `client-tool-result` events through.\n */\n readonly events: AsyncIterable<IAiStreamEvent>;\n /**\n * Resolves when the stream terminates. On success, carries the\n * {@link AiAssist.IAiClientToolTurnResult} with the optional continuation for the\n * next round. On failure, carries the error message.\n */\n readonly nextTurn: Promise<Result<IAiClientToolTurnResult>>;\n}\n\n// ============================================================================\n// executeClientToolTurn\n// ============================================================================\n\n/**\n * Orchestrates a single client-tool streaming turn for any supported provider.\n *\n * Starts a streaming request, iterates the underlying provider stream, and:\n * - Forwards `text-delta`, `tool-event`, `client-tool-call-start`, and\n * `client-tool-call-done` events through to the consumer.\n * - For each `client-tool-call-done` event: validates the raw args against the\n * tool's `parametersSchema`, invokes `execute(typedArgs)`, and emits a\n * `client-tool-result` event.\n * - After stream completion: builds the per-provider continuation (or\n * `{ continuation: undefined }` when no tool calls occurred) and resolves\n * `nextTurn`.\n *\n * **Anthropic constraint (E3):** The continuation for Anthropic does not set\n * a forced `tool_choice`. Only `tool_choice: 'auto'` (the default, i.e.\n * omitted) is compatible with extended thinking.\n *\n * @param params - Turn parameters\n * @returns `{ events, nextTurn }` — stream iterable + completion promise\n * @public\n */\nexport function executeClientToolTurn(\n params: IExecuteClientToolTurnParams\n): Result<IExecuteClientToolTurnResult> {\n const {\n descriptor,\n apiKey,\n system,\n messages,\n continuationMessages,\n temperature,\n tools,\n clientTools,\n signal,\n logger,\n resolvedThinking,\n model,\n endpoint\n } = params;\n\n const splitResult = splitChatRequest(system, messages);\n if (splitResult.isFailure()) {\n return fail(splitResult.message);\n }\n const { prompt, head } = splitResult.value;\n if (prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {\n return fail(`provider \"${descriptor.id}\" does not accept image input`);\n }\n\n // Build a lookup map of client tools by name for fast access.\n // Fail fast on duplicate names — silently overwriting would cause one tool\n // to shadow another with no observable signal.\n const toolsByName = new Map<string, IAiClientTool>();\n for (const tool of clientTools) {\n if (toolsByName.has(tool.config.name)) {\n return fail(`executeClientToolTurn: duplicate client tool name '${tool.config.name}'`);\n }\n toolsByName.set(tool.config.name, tool);\n }\n\n // Merge server tools and client tool configs into a single array for the provider.\n // This is the fix for P1-1: client tools were never sent to the provider because\n // the adapters only received `tools` (server tools). Both must coexist per design §2.5.\n const effectiveTools: ReadonlyArray<AiToolConfig> | undefined =\n clientTools.length > 0 ? [...(tools ?? []), ...clientTools.map((t) => t.config)] : tools;\n\n const effectiveTemperature = temperature ?? 0.7;\n const resolvedModel = model ?? resolveModel(descriptor.defaultModel);\n if (resolvedModel.length === 0) {\n return fail(`provider \"${descriptor.id}\": no model resolved; pass model or set descriptor.defaultModel`);\n }\n const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);\n if (baseUrlResult.isFailure()) {\n return fail(baseUrlResult.message);\n }\n const config: IStreamApiConfig = {\n baseUrl: baseUrlResult.value,\n apiKey,\n model: resolvedModel\n };\n\n // Accumulation buffers — populated by the adapter, read by the builder.\n const anthropicBuffer = new Map<number, IAccumulatedBlock>();\n const openAiCallMap = new Map<string, IAccumulatedFunctionCall>();\n const geminiCalls: IAccumulatedGeminiFunctionCall[] = [];\n\n // Collected tool results, populated as each client-tool-call-done is processed.\n const toolResults: IToolCallResult[] = [];\n\n // Stream start: open the underlying adapter stream.\n const streamPromise: Promise<Result<AsyncIterable<IAiStreamEvent>>> = (() => {\n switch (descriptor.apiFormat) {\n case 'anthropic':\n return callAnthropicStream(\n config,\n prompt,\n head,\n effectiveTemperature,\n effectiveTools,\n logger,\n signal,\n resolvedThinking,\n anthropicBuffer,\n continuationMessages\n );\n case 'openai':\n return callOpenAiResponsesStream(\n config,\n prompt,\n /* c8 ignore next 1 - defensive: openai path requires tools; empty array fallback unreachable in practice */\n effectiveTools ?? [],\n head,\n effectiveTemperature,\n logger,\n signal,\n resolvedThinking,\n openAiCallMap,\n continuationMessages\n );\n case 'gemini':\n return callGeminiStream(\n config,\n prompt,\n head,\n effectiveTemperature,\n effectiveTools,\n logger,\n signal,\n resolvedThinking,\n geminiCalls,\n continuationMessages\n );\n /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = descriptor.apiFormat;\n return Promise.resolve(fail(`unsupported API format: ${String(_exhaustive)}`));\n }\n }\n })();\n\n // Resolve controls for `nextTurn`.\n let resolveNextTurn!: (result: Result<IAiClientToolTurnResult>) => void;\n const nextTurn = new Promise<Result<IAiClientToolTurnResult>>((resolve) => {\n resolveNextTurn = resolve;\n });\n\n // The unified-event generator: opens the stream, proxies events, executes tools.\n async function* eventGenerator(): AsyncGenerator<IAiStreamEvent> {\n const streamResult = await streamPromise;\n if (streamResult.isFailure()) {\n resolveNextTurn(fail(streamResult.message));\n yield { type: 'error', message: streamResult.message };\n return;\n }\n\n let truncated = false;\n let fullText = '';\n let streamError: string | undefined;\n\n for await (const event of streamResult.value) {\n if (event.type === 'done') {\n truncated = event.truncated;\n fullText = event.fullText;\n yield event;\n continue;\n }\n\n if (event.type === 'error') {\n streamError = event.message;\n yield event;\n continue;\n }\n\n if (event.type === 'text-delta') {\n yield event;\n continue;\n }\n\n if (event.type === 'tool-event') {\n yield event;\n continue;\n }\n\n if (event.type === 'client-tool-call-start') {\n yield event;\n continue;\n }\n\n if (event.type === 'client-tool-call-done') {\n yield event;\n\n const { toolName, callId, args } = event;\n const tool = toolsByName.get(toolName);\n\n if (!tool) {\n const errMsg = `model called unknown tool: ${toolName}`;\n const resultEvent: IAiStreamEvent = {\n type: 'client-tool-result',\n toolName,\n callId,\n result: errMsg,\n isError: true\n };\n yield resultEvent;\n toolResults.push({ toolName, callId, args, result: errMsg, isError: true });\n resolveNextTurn(fail(errMsg));\n return;\n }\n\n const validationResult = tool.config.parametersSchema.validate(args);\n if (validationResult.isFailure()) {\n const errMsg = `${toolName} (callId=${callId}): ${validationResult.message}`;\n const resultEvent: IAiStreamEvent = {\n type: 'client-tool-result',\n toolName,\n callId,\n result: errMsg,\n isError: true\n };\n yield resultEvent;\n toolResults.push({ toolName, callId, args, result: errMsg, isError: true });\n continue;\n }\n\n const executeResult = await captureAsyncResult(async () => tool.execute(validationResult.value));\n const executionResult: Result<unknown> = executeResult.isSuccess()\n ? executeResult.value\n : executeResult;\n\n if (executionResult.isFailure()) {\n const errMsg = `${toolName} (callId=${callId}): ${executionResult.message}`;\n const resultEvent: IAiStreamEvent = {\n type: 'client-tool-result',\n toolName,\n callId,\n result: errMsg,\n isError: true\n };\n yield resultEvent;\n toolResults.push({ toolName, callId, args, result: errMsg, isError: true });\n continue;\n }\n\n // JSON.stringify can throw (circular references) or return undefined\n // (e.g. for a bare `undefined` value, or a value of type `function`).\n // Either outcome violates the client-tool-result event contract, so\n // emit an isError result with diagnostic context instead.\n let resultStr: string;\n try {\n const stringified = JSON.stringify(executionResult.value);\n if (stringified === undefined) {\n const errMsg = `${toolName} (callId=${callId}): tool returned a non-serializable value (JSON.stringify produced undefined)`;\n const resultEvent: IAiStreamEvent = {\n type: 'client-tool-result',\n toolName,\n callId,\n result: errMsg,\n isError: true\n };\n yield resultEvent;\n toolResults.push({ toolName, callId, args, result: errMsg, isError: true });\n continue;\n }\n resultStr = stringified;\n } catch (e) {\n const errMsg = `${toolName} (callId=${callId}): failed to serialize tool result: ${\n (e as Error).message\n }`;\n const resultEvent: IAiStreamEvent = {\n type: 'client-tool-result',\n toolName,\n callId,\n result: errMsg,\n isError: true\n };\n yield resultEvent;\n toolResults.push({ toolName, callId, args, result: errMsg, isError: true });\n continue;\n }\n const resultEvent: IAiStreamEvent = {\n type: 'client-tool-result',\n toolName,\n callId,\n result: resultStr,\n isError: false\n };\n yield resultEvent;\n toolResults.push({ toolName, callId, args, result: resultStr, isError: false });\n continue;\n }\n\n // client-tool-result events are emitted by this layer, not the adapters — no passthrough needed.\n }\n\n // Stream has ended. Build the continuation.\n if (streamError !== undefined) {\n resolveNextTurn(fail(streamError));\n return;\n }\n\n if (toolResults.length === 0) {\n resolveNextTurn(succeed({ continuation: undefined, truncated, fullText }));\n return;\n }\n\n let continuation: IAiClientToolContinuation;\n switch (descriptor.apiFormat) {\n case 'anthropic':\n continuation = buildAnthropicContinuation(anthropicBuffer, toolResults);\n break;\n case 'openai':\n continuation = buildOpenAiContinuation(openAiCallMap, toolResults);\n break;\n case 'gemini':\n continuation = buildGeminiContinuation(geminiCalls, toolResults);\n break;\n /* c8 ignore next 5 - defensive coding: exhaustive switch guaranteed by TypeScript */\n default: {\n const _exhaustive: never = descriptor.apiFormat;\n resolveNextTurn(fail(`unsupported API format: ${String(_exhaustive)}`));\n return;\n }\n }\n\n resolveNextTurn(succeed({ continuation, truncated, fullText }));\n }\n\n return succeed({\n events: { [Symbol.asyncIterator]: () => eventGenerator() },\n nextTurn\n });\n}\n"]}
|