newmark-agent 0.5.7 → 0.5.8
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/conversation-utility-host.bundle.cjs +159 -40
- package/dist/core/agent.d.ts +1 -1
- package/dist/core/agent.js +7 -1
- package/dist/core/agentKernelRunner.js +43 -8
- package/dist/core/emptyResponseRetry.d.ts +16 -0
- package/dist/core/emptyResponseRetry.js +25 -0
- package/dist/core/terminalOutputBuffer.d.ts +24 -0
- package/dist/core/terminalOutputBuffer.js +92 -0
- package/dist/llm/provider.d.ts +4 -4
- package/dist/llm/provider.js +55 -21
- package/dist/main.js +12 -3
- package/dist/providers/chat-completions.adapter.js +17 -4
- package/dist/providers/provider-events.d.ts +7 -0
- package/dist/providers/provider-events.js +40 -0
- package/dist/providers/responses.adapter.js +23 -9
- package/dist/ui/index.html +68 -17
- package/dist/wsl-agent-host.bundle.cjs +159 -40
- package/package.json +3 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TerminalOutputBuffer = exports.TERMINAL_HISTORY_LIMIT = void 0;
|
|
4
|
+
exports.TERMINAL_HISTORY_LIMIT = 256 * 1024;
|
|
5
|
+
class TerminalOutputBuffer {
|
|
6
|
+
send;
|
|
7
|
+
sessions = new Map();
|
|
8
|
+
timer = null;
|
|
9
|
+
flushIntervalMs;
|
|
10
|
+
historyLimit;
|
|
11
|
+
constructor(send, options = {}) {
|
|
12
|
+
this.send = send;
|
|
13
|
+
this.flushIntervalMs = Math.max(1, options.flushIntervalMs ?? 20);
|
|
14
|
+
this.historyLimit = Math.max(1, options.historyLimit ?? exports.TERMINAL_HISTORY_LIMIT);
|
|
15
|
+
}
|
|
16
|
+
push(sessionId, text) {
|
|
17
|
+
if (!text)
|
|
18
|
+
return;
|
|
19
|
+
let state = this.sessions.get(sessionId);
|
|
20
|
+
if (!state) {
|
|
21
|
+
state = { chunks: [], length: 0, history: '' };
|
|
22
|
+
this.sessions.set(sessionId, state);
|
|
23
|
+
}
|
|
24
|
+
state.chunks.push(text);
|
|
25
|
+
state.length += text.length;
|
|
26
|
+
this.schedule();
|
|
27
|
+
}
|
|
28
|
+
flush(sessionId) {
|
|
29
|
+
const state = this.sessions.get(sessionId);
|
|
30
|
+
if (!state?.length)
|
|
31
|
+
return;
|
|
32
|
+
const text = state.chunks.length === 1 ? state.chunks[0] : state.chunks.join('');
|
|
33
|
+
state.chunks = [];
|
|
34
|
+
state.length = 0;
|
|
35
|
+
state.history = this.bound(`${state.history}${text}`);
|
|
36
|
+
this.send(sessionId, text);
|
|
37
|
+
this.clearTimerWhenIdle();
|
|
38
|
+
}
|
|
39
|
+
flushAll() {
|
|
40
|
+
for (const sessionId of this.sessions.keys())
|
|
41
|
+
this.flush(sessionId);
|
|
42
|
+
this.clearTimerWhenIdle(true);
|
|
43
|
+
}
|
|
44
|
+
close(sessionId) {
|
|
45
|
+
this.flush(sessionId);
|
|
46
|
+
const history = this.sessions.get(sessionId)?.history ?? '';
|
|
47
|
+
this.sessions.delete(sessionId);
|
|
48
|
+
this.clearTimerWhenIdle();
|
|
49
|
+
return history;
|
|
50
|
+
}
|
|
51
|
+
history(sessionId) {
|
|
52
|
+
const state = this.sessions.get(sessionId);
|
|
53
|
+
if (!state)
|
|
54
|
+
return '';
|
|
55
|
+
if (!state.length)
|
|
56
|
+
return state.history;
|
|
57
|
+
const pending = state.chunks.length === 1 ? state.chunks[0] : state.chunks.join('');
|
|
58
|
+
return this.bound(`${state.history}${pending}`);
|
|
59
|
+
}
|
|
60
|
+
pendingChunkCount() {
|
|
61
|
+
let count = 0;
|
|
62
|
+
for (const state of this.sessions.values())
|
|
63
|
+
count += state.chunks.length;
|
|
64
|
+
return count;
|
|
65
|
+
}
|
|
66
|
+
hasScheduledFlush() {
|
|
67
|
+
return this.timer !== null;
|
|
68
|
+
}
|
|
69
|
+
bound(text) {
|
|
70
|
+
return text.length > this.historyLimit ? text.slice(-this.historyLimit) : text;
|
|
71
|
+
}
|
|
72
|
+
schedule() {
|
|
73
|
+
if (this.timer)
|
|
74
|
+
return;
|
|
75
|
+
this.timer = setTimeout(() => {
|
|
76
|
+
this.timer = null;
|
|
77
|
+
this.flushAll();
|
|
78
|
+
}, this.flushIntervalMs);
|
|
79
|
+
this.timer.unref?.();
|
|
80
|
+
}
|
|
81
|
+
clearTimerWhenIdle(force = false) {
|
|
82
|
+
if (!this.timer)
|
|
83
|
+
return;
|
|
84
|
+
const hasPending = !force && Array.from(this.sessions.values()).some(state => state.length > 0);
|
|
85
|
+
if (hasPending)
|
|
86
|
+
return;
|
|
87
|
+
clearTimeout(this.timer);
|
|
88
|
+
this.timer = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.TerminalOutputBuffer = TerminalOutputBuffer;
|
|
92
|
+
//# sourceMappingURL=terminalOutputBuffer.js.map
|
package/dist/llm/provider.d.ts
CHANGED
|
@@ -96,7 +96,7 @@ export declare class LLMProvider {
|
|
|
96
96
|
* `provider_adapters_v2` context flag. Request serialization and SSE
|
|
97
97
|
* normalization are delegated to the shared provider adapters while the
|
|
98
98
|
* transport orchestration (loopback node-http, fetch -> node-http fallback,
|
|
99
|
-
*
|
|
99
|
+
* cancellation-only streaming and the 4xx Chat -> Responses downgrade) stay here.
|
|
100
100
|
* The emitted request body and StreamToken stream are byte-equivalent to
|
|
101
101
|
* the legacy inlined path.
|
|
102
102
|
*/
|
|
@@ -106,9 +106,9 @@ export declare class LLMProvider {
|
|
|
106
106
|
private shouldDowngradeToResponses;
|
|
107
107
|
/**
|
|
108
108
|
* Loopback-aware transport injected into adapter `execute`. Streaming
|
|
109
|
-
* requests retain the fetch-to-node fallback for transport failures
|
|
110
|
-
*
|
|
111
|
-
*
|
|
109
|
+
* requests retain the fetch-to-node fallback for transport failures. They
|
|
110
|
+
* have no response deadline; only caller cancellation or a concrete
|
|
111
|
+
* transport/provider failure may end the request.
|
|
112
112
|
*/
|
|
113
113
|
private buildProviderAdapterTransport;
|
|
114
114
|
private toTransportResponse;
|
package/dist/llm/provider.js
CHANGED
|
@@ -43,9 +43,6 @@ const child_process_1 = require("child_process");
|
|
|
43
43
|
const agentKernelDiagnostics_1 = require("../core/agentKernelDiagnostics");
|
|
44
44
|
const chat_messages_1 = require("../providers/chat-messages");
|
|
45
45
|
const providers_1 = require("../providers");
|
|
46
|
-
// Keep provider requests below the release-harness/user-visible command
|
|
47
|
-
// deadline. A provider that does not answer must produce one bounded error;
|
|
48
|
-
// it must not restart the same request through every Windows transport.
|
|
49
46
|
// Provider responses are intentionally unbounded. User cancellation, transport
|
|
50
47
|
// errors, and tool-specific limits remain the only automatic stop conditions.
|
|
51
48
|
const DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 0;
|
|
@@ -939,7 +936,7 @@ class LLMProvider {
|
|
|
939
936
|
* `provider_adapters_v2` context flag. Request serialization and SSE
|
|
940
937
|
* normalization are delegated to the shared provider adapters while the
|
|
941
938
|
* transport orchestration (loopback node-http, fetch -> node-http fallback,
|
|
942
|
-
*
|
|
939
|
+
* cancellation-only streaming and the 4xx Chat -> Responses downgrade) stay here.
|
|
943
940
|
* The emitted request body and StreamToken stream are byte-equivalent to
|
|
944
941
|
* the legacy inlined path.
|
|
945
942
|
*/
|
|
@@ -1082,9 +1079,9 @@ class LLMProvider {
|
|
|
1082
1079
|
}
|
|
1083
1080
|
/**
|
|
1084
1081
|
* Loopback-aware transport injected into adapter `execute`. Streaming
|
|
1085
|
-
* requests retain the fetch-to-node fallback for transport failures
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1082
|
+
* requests retain the fetch-to-node fallback for transport failures. They
|
|
1083
|
+
* have no response deadline; only caller cancellation or a concrete
|
|
1084
|
+
* transport/provider failure may end the request.
|
|
1088
1085
|
*/
|
|
1089
1086
|
buildProviderAdapterTransport() {
|
|
1090
1087
|
return async (request, signal) => {
|
|
@@ -1096,7 +1093,9 @@ class LLMProvider {
|
|
|
1096
1093
|
forwardAbort();
|
|
1097
1094
|
else
|
|
1098
1095
|
signal?.addEventListener('abort', forwardAbort, { once: true });
|
|
1099
|
-
|
|
1096
|
+
// Streaming provider responses are intentionally unbounded. Only
|
|
1097
|
+
// caller cancellation or an explicit provider failure may end them.
|
|
1098
|
+
const effectiveTimeout = 0;
|
|
1100
1099
|
const timer = effectiveTimeout > 0
|
|
1101
1100
|
? setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout)
|
|
1102
1101
|
: undefined;
|
|
@@ -1247,7 +1246,9 @@ class LLMProvider {
|
|
|
1247
1246
|
forwardAbort();
|
|
1248
1247
|
else
|
|
1249
1248
|
signal?.addEventListener('abort', forwardAbort, { once: true });
|
|
1250
|
-
|
|
1249
|
+
// Streaming provider responses are intentionally unbounded. Do not turn
|
|
1250
|
+
// silence into an empty-response failure.
|
|
1251
|
+
const effectiveTimeout = 0;
|
|
1251
1252
|
const timeout = effectiveTimeout > 0
|
|
1252
1253
|
? setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout)
|
|
1253
1254
|
: undefined;
|
|
@@ -1285,10 +1286,14 @@ class LLMProvider {
|
|
|
1285
1286
|
}
|
|
1286
1287
|
const decoder = new TextDecoder();
|
|
1287
1288
|
let buffer = '';
|
|
1288
|
-
|
|
1289
|
+
const toolCalls = new Map();
|
|
1290
|
+
const toolCallOrder = [];
|
|
1291
|
+
let syntheticToolIndex = 0;
|
|
1292
|
+
let lastToolIndex = 0;
|
|
1289
1293
|
let currentReasoningContent = '';
|
|
1290
1294
|
let contentPolicyBlocked = false;
|
|
1291
1295
|
let emittedContent = false;
|
|
1296
|
+
let explicitCompletion = false;
|
|
1292
1297
|
const streamSignal = signal || new AbortController().signal;
|
|
1293
1298
|
while (true) {
|
|
1294
1299
|
const { done, value } = await (0, providers_1.readProviderStreamChunk)(reader, streamSignal);
|
|
@@ -1302,8 +1307,10 @@ class LLMProvider {
|
|
|
1302
1307
|
if (!trimmed.startsWith('data: '))
|
|
1303
1308
|
continue;
|
|
1304
1309
|
const data = trimmed.slice(6);
|
|
1305
|
-
if (data === '[DONE]')
|
|
1310
|
+
if (data === '[DONE]') {
|
|
1311
|
+
explicitCompletion = true;
|
|
1306
1312
|
continue;
|
|
1313
|
+
}
|
|
1307
1314
|
try {
|
|
1308
1315
|
const json = JSON.parse(data);
|
|
1309
1316
|
if (json.usage)
|
|
@@ -1324,27 +1331,54 @@ class LLMProvider {
|
|
|
1324
1331
|
}
|
|
1325
1332
|
if (delta.tool_calls) {
|
|
1326
1333
|
for (const tc of delta.tool_calls) {
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1334
|
+
const rawIndex = Number(tc.index);
|
|
1335
|
+
const index = Number.isInteger(rawIndex) && rawIndex >= 0
|
|
1336
|
+
? rawIndex
|
|
1337
|
+
: (tc.id ? syntheticToolIndex++ : lastToolIndex);
|
|
1338
|
+
lastToolIndex = index;
|
|
1339
|
+
let call = toolCalls.get(index);
|
|
1340
|
+
if (!call && (tc.id || tc.function?.name)) {
|
|
1341
|
+
call = { id: tc.id || '', name: tc.function?.name || '', argumentParts: [] };
|
|
1342
|
+
toolCalls.set(index, call);
|
|
1343
|
+
toolCallOrder.push(index);
|
|
1335
1344
|
}
|
|
1345
|
+
if (!call)
|
|
1346
|
+
continue;
|
|
1347
|
+
if (tc.id && !call.id)
|
|
1348
|
+
call.id = tc.id;
|
|
1349
|
+
if (tc.function?.name && !call.name)
|
|
1350
|
+
call.name = tc.function.name;
|
|
1351
|
+
if (tc.function?.arguments)
|
|
1352
|
+
call.argumentParts.push(tc.function.arguments);
|
|
1336
1353
|
}
|
|
1337
1354
|
}
|
|
1338
1355
|
}
|
|
1339
1356
|
catch { /* skip malformed JSON */ }
|
|
1340
1357
|
}
|
|
1341
1358
|
}
|
|
1342
|
-
if (
|
|
1343
|
-
|
|
1359
|
+
if (toolCallOrder.length) {
|
|
1360
|
+
for (const index of toolCallOrder) {
|
|
1361
|
+
const call = toolCalls.get(index);
|
|
1362
|
+
if (!call)
|
|
1363
|
+
continue;
|
|
1364
|
+
yield {
|
|
1365
|
+
type: 'tool_call',
|
|
1366
|
+
text: '',
|
|
1367
|
+
toolCall: {
|
|
1368
|
+
id: call.id,
|
|
1369
|
+
name: call.name,
|
|
1370
|
+
arguments: (0, providers_1.assembleCompatibleToolArguments)(call.argumentParts),
|
|
1371
|
+
},
|
|
1372
|
+
reasoningContent: currentReasoningContent || undefined,
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1344
1375
|
}
|
|
1345
1376
|
else if (!emittedContent && contentPolicyBlocked) {
|
|
1346
1377
|
yield { type: 'text', text: '[Error] Content policy refusal (content_filter).' };
|
|
1347
1378
|
}
|
|
1379
|
+
else if (!explicitCompletion && !emittedContent && !currentReasoningContent) {
|
|
1380
|
+
yield { type: 'text', text: '[LLM Error] GitHub Models stream ended before an explicit completion.' };
|
|
1381
|
+
}
|
|
1348
1382
|
}
|
|
1349
1383
|
finally {
|
|
1350
1384
|
reader?.releaseLock();
|
package/dist/main.js
CHANGED
|
@@ -77,6 +77,7 @@ const compat_1 = require("./core/compat");
|
|
|
77
77
|
const dshCompatibility_1 = require("./core/dshCompatibility");
|
|
78
78
|
const mcpManager_1 = require("./core/mcpManager");
|
|
79
79
|
const workEventCoalescer_1 = require("./core/workEventCoalescer");
|
|
80
|
+
const terminalOutputBuffer_1 = require("./core/terminalOutputBuffer");
|
|
80
81
|
const cli_help_1 = require("./cli-help");
|
|
81
82
|
const APP_NAME = 'Newmark Agent';
|
|
82
83
|
const APP_ID = 'ai.newmark.agent';
|
|
@@ -4451,10 +4452,16 @@ else {
|
|
|
4451
4452
|
}
|
|
4452
4453
|
});
|
|
4453
4454
|
const ptySessions = new Map();
|
|
4454
|
-
const
|
|
4455
|
-
session
|
|
4455
|
+
const terminalOutput = new terminalOutputBuffer_1.TerminalOutputBuffer((sessionId, text) => {
|
|
4456
|
+
const session = ptySessions.get(sessionId);
|
|
4457
|
+
if (session)
|
|
4458
|
+
session.buffer = terminalOutput.history(sessionId);
|
|
4456
4459
|
if (mainWindow && !mainWindow.isDestroyed())
|
|
4457
4460
|
mainWindow.webContents.send('pty:data', sessionId, text);
|
|
4461
|
+
});
|
|
4462
|
+
const sendTerminalData = (sessionId, session, text) => {
|
|
4463
|
+
terminalOutput.push(sessionId, text);
|
|
4464
|
+
session.buffer = terminalOutput.history(sessionId);
|
|
4458
4465
|
};
|
|
4459
4466
|
electron_1.ipcMain.handle('pty:spawn', async (_event, shellId) => {
|
|
4460
4467
|
const sessionId = (0, crypto_1.randomUUID)().slice(0, 8);
|
|
@@ -4476,7 +4483,9 @@ else {
|
|
|
4476
4483
|
ptySessions.set(sessionId, session);
|
|
4477
4484
|
proc.onData(text => sendTerminalData(sessionId, session, text));
|
|
4478
4485
|
proc.onExit(event => {
|
|
4486
|
+
terminalOutput.flush(sessionId);
|
|
4479
4487
|
ptySessions.delete(sessionId);
|
|
4488
|
+
terminalOutput.close(sessionId);
|
|
4480
4489
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
4481
4490
|
mainWindow.webContents.send('pty:exit', sessionId, event.exitCode);
|
|
4482
4491
|
}
|
|
@@ -4534,7 +4543,7 @@ else {
|
|
|
4534
4543
|
const session = ptySessions.get(sessionId);
|
|
4535
4544
|
if (!session)
|
|
4536
4545
|
return { buffer: '' };
|
|
4537
|
-
return { buffer: session.buffer };
|
|
4546
|
+
return { buffer: terminalOutput.history(sessionId) || session.buffer };
|
|
4538
4547
|
});
|
|
4539
4548
|
const terminalOwnerFor = (conversationId, actorId) => (0, terminalTakeover_1.normalizeTerminalTakeoverOwner)({
|
|
4540
4549
|
backend: wslBackendEnabled() ? 'wsl' : (process.platform === 'win32' ? 'windows' : process.platform),
|
|
@@ -105,6 +105,8 @@ class ChatCompletionsAdapter {
|
|
|
105
105
|
let contentPolicyBlocked = false;
|
|
106
106
|
let emittedContent = false;
|
|
107
107
|
let emittedTool = false;
|
|
108
|
+
let emittedReasoning = false;
|
|
109
|
+
let explicitCompletion = false;
|
|
108
110
|
try {
|
|
109
111
|
while (true) {
|
|
110
112
|
const { done, value } = await (0, provider_events_1.readProviderStreamChunk)(reader, signal);
|
|
@@ -118,8 +120,10 @@ class ChatCompletionsAdapter {
|
|
|
118
120
|
if (!trimmed.startsWith('data: '))
|
|
119
121
|
continue;
|
|
120
122
|
const data = trimmed.slice(6);
|
|
121
|
-
if (data === '[DONE]')
|
|
123
|
+
if (data === '[DONE]') {
|
|
124
|
+
explicitCompletion = true;
|
|
122
125
|
continue;
|
|
126
|
+
}
|
|
123
127
|
let json;
|
|
124
128
|
try {
|
|
125
129
|
json = JSON.parse(data);
|
|
@@ -135,13 +139,18 @@ class ChatCompletionsAdapter {
|
|
|
135
139
|
if ((0, provider_events_1.isContentPolicyBlocked)(json))
|
|
136
140
|
contentPolicyBlocked = true;
|
|
137
141
|
const choices = Array.isArray(json.choices) ? json.choices : [];
|
|
138
|
-
const
|
|
142
|
+
const choice = choices[0];
|
|
143
|
+
if (choice?.finish_reason !== undefined && choice.finish_reason !== null)
|
|
144
|
+
explicitCompletion = true;
|
|
145
|
+
const delta = choice?.delta;
|
|
139
146
|
if (!delta)
|
|
140
147
|
continue;
|
|
141
148
|
if (delta.reasoning_content) {
|
|
142
149
|
const reasoning = this.extractText(delta.reasoning_content);
|
|
143
|
-
if (reasoning)
|
|
150
|
+
if (reasoning) {
|
|
151
|
+
emittedReasoning = true;
|
|
144
152
|
yield { type: 'reasoning.summary.delta', delta: reasoning };
|
|
153
|
+
}
|
|
145
154
|
}
|
|
146
155
|
const textDelta = this.extractText(delta.content);
|
|
147
156
|
if (textDelta) {
|
|
@@ -190,7 +199,7 @@ class ChatCompletionsAdapter {
|
|
|
190
199
|
type: 'tool_call.completed',
|
|
191
200
|
id: currentToolCall.id,
|
|
192
201
|
name: currentToolCall.name,
|
|
193
|
-
arguments: currentToolCall.argumentParts
|
|
202
|
+
arguments: (0, provider_events_1.assembleCompatibleToolArguments)(currentToolCall.argumentParts),
|
|
194
203
|
};
|
|
195
204
|
}
|
|
196
205
|
}
|
|
@@ -198,6 +207,10 @@ class ChatCompletionsAdapter {
|
|
|
198
207
|
yield { type: 'response.failed', error: '[Error] Content policy refusal (content_filter).' };
|
|
199
208
|
return;
|
|
200
209
|
}
|
|
210
|
+
if (!explicitCompletion && !emittedContent && !emittedTool && !emittedReasoning) {
|
|
211
|
+
yield { type: 'response.failed', error: '[LLM Error] Chat stream ended before an explicit completion.' };
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
201
214
|
yield { type: 'response.completed' };
|
|
202
215
|
}
|
|
203
216
|
finally {
|
|
@@ -29,6 +29,13 @@ export declare function parseProviderSse(raw: string): Array<{
|
|
|
29
29
|
event?: string;
|
|
30
30
|
data: string;
|
|
31
31
|
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Compatible gateways may stream function arguments as JSON deltas or repeat
|
|
34
|
+
* a cumulative snapshot on every SSE frame. Keep the normal incremental form
|
|
35
|
+
* when it parses, then fall back to snapshot folding. Returning malformed
|
|
36
|
+
* concatenated snapshots would erase the model's correction at tool parsing.
|
|
37
|
+
*/
|
|
38
|
+
export declare function assembleCompatibleToolArguments(parts: string[]): string;
|
|
32
39
|
/**
|
|
33
40
|
* Detect content-policy refusals across the provider failure shapes.
|
|
34
41
|
* Mirrors `LLMProvider.contentPolicyBlocked` semantics exactly so the
|
|
@@ -5,6 +5,7 @@ exports.providerAbortError = providerAbortError;
|
|
|
5
5
|
exports.providerStreamTimeoutError = providerStreamTimeoutError;
|
|
6
6
|
exports.readProviderStreamChunk = readProviderStreamChunk;
|
|
7
7
|
exports.parseProviderSse = parseProviderSse;
|
|
8
|
+
exports.assembleCompatibleToolArguments = assembleCompatibleToolArguments;
|
|
8
9
|
exports.isContentPolicyBlocked = isContentPolicyBlocked;
|
|
9
10
|
exports.normalizeProviderUsage = normalizeProviderUsage;
|
|
10
11
|
exports.estimateRequestTokens = estimateRequestTokens;
|
|
@@ -105,6 +106,45 @@ function parseProviderSse(raw) {
|
|
|
105
106
|
}
|
|
106
107
|
return events;
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Compatible gateways may stream function arguments as JSON deltas or repeat
|
|
111
|
+
* a cumulative snapshot on every SSE frame. Keep the normal incremental form
|
|
112
|
+
* when it parses, then fall back to snapshot folding. Returning malformed
|
|
113
|
+
* concatenated snapshots would erase the model's correction at tool parsing.
|
|
114
|
+
*/
|
|
115
|
+
function assembleCompatibleToolArguments(parts) {
|
|
116
|
+
const nonEmpty = (parts || []).map(String).filter(part => part && part !== 'null');
|
|
117
|
+
if (!nonEmpty.length)
|
|
118
|
+
return '{}';
|
|
119
|
+
const isJsonObject = (value) => {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(value);
|
|
122
|
+
return !!parsed && typeof parsed === 'object' && !Array.isArray(parsed);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const incremental = nonEmpty.join('');
|
|
129
|
+
if (isJsonObject(incremental))
|
|
130
|
+
return incremental;
|
|
131
|
+
let compatible = '';
|
|
132
|
+
for (const incoming of nonEmpty) {
|
|
133
|
+
if (!compatible)
|
|
134
|
+
compatible = incoming;
|
|
135
|
+
else if (incoming === compatible)
|
|
136
|
+
continue;
|
|
137
|
+
else if (incoming.startsWith(compatible))
|
|
138
|
+
compatible = incoming;
|
|
139
|
+
else if (compatible.startsWith(incoming))
|
|
140
|
+
continue;
|
|
141
|
+
else
|
|
142
|
+
compatible += incoming;
|
|
143
|
+
}
|
|
144
|
+
if (isJsonObject(compatible))
|
|
145
|
+
return compatible;
|
|
146
|
+
return [...nonEmpty].reverse().find(isJsonObject) || compatible;
|
|
147
|
+
}
|
|
108
148
|
/**
|
|
109
149
|
* Detect content-policy refusals across the provider failure shapes.
|
|
110
150
|
* Mirrors `LLMProvider.contentPolicyBlocked` semantics exactly so the
|
|
@@ -155,6 +155,7 @@ class ResponsesAdapter {
|
|
|
155
155
|
const key = `${String(payload.item_id || '')}:${String(payload.summary_index || 0)}`;
|
|
156
156
|
const delta = this.extractText(payload.delta);
|
|
157
157
|
if (delta) {
|
|
158
|
+
emittedContent = true;
|
|
158
159
|
reasoningSummaries.set(key, (reasoningSummaries.get(key) || '') + delta);
|
|
159
160
|
yield { type: 'reasoning.summary.delta', delta };
|
|
160
161
|
}
|
|
@@ -185,7 +186,7 @@ class ResponsesAdapter {
|
|
|
185
186
|
calls.set(key, {
|
|
186
187
|
id: String(item.call_id || item.id || key),
|
|
187
188
|
name: String(item.name || ''),
|
|
188
|
-
|
|
189
|
+
argumentParts: item.arguments ? [String(item.arguments)] : [],
|
|
189
190
|
emitted: false,
|
|
190
191
|
});
|
|
191
192
|
}
|
|
@@ -193,9 +194,10 @@ class ResponsesAdapter {
|
|
|
193
194
|
}
|
|
194
195
|
if (eventType === 'response.function_call_arguments.delta') {
|
|
195
196
|
const key = String(payload.item_id || payload.call_id || payload.output_index || '');
|
|
196
|
-
const call = calls.get(key) || { id: String(payload.call_id || key), name: String(payload.name || ''),
|
|
197
|
+
const call = calls.get(key) || { id: String(payload.call_id || key), name: String(payload.name || ''), argumentParts: [], emitted: false };
|
|
197
198
|
const delta = String(payload.delta || '');
|
|
198
|
-
|
|
199
|
+
if (delta)
|
|
200
|
+
call.argumentParts.push(delta);
|
|
199
201
|
calls.set(key, call);
|
|
200
202
|
yield { type: 'tool_call.arguments.delta', id: call.id, delta };
|
|
201
203
|
continue;
|
|
@@ -207,19 +209,21 @@ class ResponsesAdapter {
|
|
|
207
209
|
const call = calls.get(key) || {
|
|
208
210
|
id: String(item.call_id || item.id || key),
|
|
209
211
|
name: String(item.name || ''),
|
|
210
|
-
|
|
212
|
+
argumentParts: item.arguments ? [String(item.arguments)] : [],
|
|
211
213
|
emitted: false,
|
|
212
214
|
};
|
|
213
215
|
call.id = String(item.call_id || call.id);
|
|
214
216
|
call.name = String(item.name || call.name);
|
|
215
|
-
|
|
217
|
+
if (typeof item.arguments === 'string' && item.arguments)
|
|
218
|
+
call.argumentParts.push(item.arguments);
|
|
216
219
|
if (!call.emitted) {
|
|
217
220
|
call.emitted = true;
|
|
221
|
+
const argumentsJson = (0, provider_events_1.assembleCompatibleToolArguments)(call.argumentParts);
|
|
218
222
|
yield { type: 'tool_call.started', id: call.id, name: call.name };
|
|
219
|
-
if (
|
|
220
|
-
yield { type: 'tool_call.arguments.delta', id: call.id, delta:
|
|
223
|
+
if (argumentsJson !== '{}') {
|
|
224
|
+
yield { type: 'tool_call.arguments.delta', id: call.id, delta: argumentsJson };
|
|
221
225
|
}
|
|
222
|
-
yield { type: 'tool_call.completed', id: call.id, name: call.name, arguments:
|
|
226
|
+
yield { type: 'tool_call.completed', id: call.id, name: call.name, arguments: argumentsJson };
|
|
223
227
|
}
|
|
224
228
|
calls.set(key, call);
|
|
225
229
|
}
|
|
@@ -252,9 +256,19 @@ class ResponsesAdapter {
|
|
|
252
256
|
yield { type: 'response.failed', error: '[LLM Error] Responses stream ended before response.completed.' };
|
|
253
257
|
}
|
|
254
258
|
else if (!emittedContent && calls.size === 0) {
|
|
255
|
-
yield { type: 'response.failed', error: '[Error]
|
|
259
|
+
yield { type: 'response.failed', error: '[Error] Provider returned an empty response.' };
|
|
256
260
|
}
|
|
257
261
|
else {
|
|
262
|
+
// Some compatible Responses providers omit output_item.done but still
|
|
263
|
+
// complete the response. Preserve that valid tool activity and fold
|
|
264
|
+
// cumulative argument snapshots before handing it to the kernel.
|
|
265
|
+
for (const call of calls.values()) {
|
|
266
|
+
if (call.emitted)
|
|
267
|
+
continue;
|
|
268
|
+
const argumentsJson = (0, provider_events_1.assembleCompatibleToolArguments)(call.argumentParts);
|
|
269
|
+
yield { type: 'tool_call.started', id: call.id, name: call.name };
|
|
270
|
+
yield { type: 'tool_call.completed', id: call.id, name: call.name, arguments: argumentsJson };
|
|
271
|
+
}
|
|
258
272
|
yield { type: 'response.completed' };
|
|
259
273
|
}
|
|
260
274
|
}
|