@yeaft/webchat-agent 0.1.702 → 0.1.703
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/package.json +1 -1
- package/unify/engine.js +11 -19
- package/unify/llm/adapter.js +8 -53
- package/unify/llm/anthropic.js +15 -34
package/package.json
CHANGED
package/unify/engine.js
CHANGED
|
@@ -74,10 +74,11 @@ const MAX_CONTINUE_TURNS = 3;
|
|
|
74
74
|
* - `toolCalls` on assistant turns (the LLM's function_call requests)
|
|
75
75
|
* - `toolCallId` + `isError` on tool turns (the paired tool_result)
|
|
76
76
|
*
|
|
77
|
-
* Content is
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* Content is passed through verbatim — never truncated. Debug traces must
|
|
78
|
+
* mirror exactly what we sent to the LLM; a truncated copy is misleading.
|
|
79
|
+
* If the resulting payload is too large for the client debug store the
|
|
80
|
+
* bound is per-loop-count (see `MAX_UNIFY_DEBUG_LOOPS` in
|
|
81
|
+
* `web/stores/chat.js`), not per-payload mutilation here.
|
|
81
82
|
*
|
|
82
83
|
* Pure function — no side effects on the input message.
|
|
83
84
|
*
|
|
@@ -86,22 +87,13 @@ const MAX_CONTINUE_TURNS = 3;
|
|
|
86
87
|
*/
|
|
87
88
|
export function mapDebugMessage(m) {
|
|
88
89
|
const out = { role: m.role };
|
|
89
|
-
out.content =
|
|
90
|
+
out.content = m.content;
|
|
90
91
|
if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
|
|
91
|
-
out.toolCalls = m.toolCalls.map(tc => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
input = { __truncated: true, preview: s.slice(0, 10000) };
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
// Non-serializable input — fall through with raw reference; the
|
|
100
|
-
// frontend's JSON.stringify will hit the same failure and replace
|
|
101
|
-
// it with a placeholder string.
|
|
102
|
-
}
|
|
103
|
-
return { id: tc.id, name: tc.name, input };
|
|
104
|
-
});
|
|
92
|
+
out.toolCalls = m.toolCalls.map(tc => ({
|
|
93
|
+
id: tc.id,
|
|
94
|
+
name: tc.name,
|
|
95
|
+
input: tc.input,
|
|
96
|
+
}));
|
|
105
97
|
}
|
|
106
98
|
if (m.toolCallId) out.toolCallId = m.toolCallId;
|
|
107
99
|
if (m.isError != null) out.isError = m.isError;
|
package/unify/llm/adapter.js
CHANGED
|
@@ -105,65 +105,20 @@ export class LLMAbortError extends Error {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
// ───
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* task-344 follow-up (N2): cap raw payload size exposed via onRawExchange.
|
|
112
|
-
* Prevents the web debug store from linear-growing when a single turn
|
|
113
|
-
* sends / receives megabytes of content. 256 KiB per field per turn.
|
|
114
|
-
*/
|
|
115
|
-
export const RAW_PAYLOAD_CAP_BYTES = 256 * 1024;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Truncate a string to at most `cap` bytes (UTF-8). When within cap the
|
|
119
|
-
* original is returned; otherwise a prefix with a trailing
|
|
120
|
-
* `…[truncated, original N bytes]` marker. Non-string inputs pass through.
|
|
121
|
-
*
|
|
122
|
-
* @param {string} s
|
|
123
|
-
* @param {number} [cap=RAW_PAYLOAD_CAP_BYTES]
|
|
124
|
-
* @returns {string}
|
|
125
|
-
*/
|
|
126
|
-
export function capRawString(s, cap = RAW_PAYLOAD_CAP_BYTES) {
|
|
127
|
-
if (typeof s !== 'string') return s;
|
|
128
|
-
const encoder = new TextEncoder();
|
|
129
|
-
const fullBytes = encoder.encode(s);
|
|
130
|
-
if (fullBytes.length <= cap) return s;
|
|
131
|
-
const decoder = new TextDecoder('utf-8', { fatal: false });
|
|
132
|
-
const prefix = decoder.decode(fullBytes.slice(0, cap));
|
|
133
|
-
return `${prefix}…[truncated, original ${fullBytes.length} bytes]`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Cap the `body` field of a rawRequest envelope. JSON-stringifies objects
|
|
138
|
-
* before sizing so an oversized `messages` array does not escape the cap.
|
|
139
|
-
* When truncation fires, body becomes a string (JSON prefix + marker);
|
|
140
|
-
* objects under cap stay objects.
|
|
141
|
-
*
|
|
142
|
-
* @param {{ url: string, method: string, headers: object, body: any }} req
|
|
143
|
-
* @param {number} [cap=RAW_PAYLOAD_CAP_BYTES]
|
|
144
|
-
* @returns {{ url: string, method: string, headers: object, body: any }}
|
|
145
|
-
*/
|
|
146
|
-
export function capRawRequest(req, cap = RAW_PAYLOAD_CAP_BYTES) {
|
|
147
|
-
if (!req || typeof req !== 'object') return req;
|
|
148
|
-
let body = req.body;
|
|
149
|
-
if (body != null && typeof body !== 'string') {
|
|
150
|
-
let serialized;
|
|
151
|
-
try { serialized = JSON.stringify(body); }
|
|
152
|
-
catch { serialized = String(body); }
|
|
153
|
-
if (typeof serialized === 'string' && new TextEncoder().encode(serialized).length > cap) {
|
|
154
|
-
body = capRawString(serialized, cap);
|
|
155
|
-
}
|
|
156
|
-
} else if (typeof body === 'string') {
|
|
157
|
-
body = capRawString(body, cap);
|
|
158
|
-
}
|
|
159
|
-
return { url: req.url, method: req.method, headers: req.headers, body };
|
|
160
|
-
}
|
|
108
|
+
// ─── Raw payload redaction helper ──────────────────────────────
|
|
161
109
|
|
|
162
110
|
/**
|
|
163
111
|
* Redact sensitive headers (API keys / bearer tokens) from a raw request
|
|
164
112
|
* shape before exposing it to debug UI. Always returns a NEW object — never
|
|
165
113
|
* mutates the input.
|
|
166
114
|
*
|
|
115
|
+
* NOTE: there is intentionally NO body / response truncation here. The whole
|
|
116
|
+
* point of the "copy request" debug feature is to capture EXACTLY what we
|
|
117
|
+
* sent to the LLM. A truncated copy is worse than useless — it lies about
|
|
118
|
+
* what the model saw. If the resulting payload is too large for the debug
|
|
119
|
+
* store, the fix is to bound retention (drop oldest turns), not to mutilate
|
|
120
|
+
* individual payloads. See `MAX_UNIFY_DEBUG_LOOPS` in `web/stores/chat.js`.
|
|
121
|
+
*
|
|
167
122
|
* @param {{ url: string, method: string, headers: object, body: any }} req
|
|
168
123
|
* @returns {{ url: string, method: string, headers: object, body: any }}
|
|
169
124
|
*/
|
package/unify/llm/anthropic.js
CHANGED
|
@@ -14,9 +14,6 @@ import {
|
|
|
14
14
|
LLMServerError,
|
|
15
15
|
LLMAbortError,
|
|
16
16
|
redactRawRequest,
|
|
17
|
-
capRawRequest,
|
|
18
|
-
capRawString,
|
|
19
|
-
RAW_PAYLOAD_CAP_BYTES,
|
|
20
17
|
} from './adapter.js';
|
|
21
18
|
import {
|
|
22
19
|
normalizeEffort,
|
|
@@ -184,9 +181,10 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
184
181
|
'anthropic-version': API_VERSION,
|
|
185
182
|
};
|
|
186
183
|
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
184
|
+
// Expose the raw request (auth-redacted) for the debug panel. The body
|
|
185
|
+
// is captured verbatim — never truncated — so "copy request" matches
|
|
186
|
+
// exactly what we POST to the LLM.
|
|
187
|
+
const rawRequest = redactRawRequest({ url, method: 'POST', headers, body });
|
|
190
188
|
|
|
191
189
|
const response = await fetch(url, {
|
|
192
190
|
method: 'POST',
|
|
@@ -197,7 +195,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
197
195
|
|
|
198
196
|
if (!response.ok) {
|
|
199
197
|
const errorBody = await response.text();
|
|
200
|
-
//
|
|
198
|
+
// Capture error response too, then throw.
|
|
201
199
|
if (onRawExchange) {
|
|
202
200
|
try {
|
|
203
201
|
onRawExchange({
|
|
@@ -207,8 +205,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
207
205
|
headers: response.headers && typeof response.headers.entries === 'function'
|
|
208
206
|
? Object.fromEntries(response.headers.entries())
|
|
209
207
|
: {},
|
|
210
|
-
|
|
211
|
-
body: capRawString(errorBody),
|
|
208
|
+
body: errorBody,
|
|
212
209
|
},
|
|
213
210
|
});
|
|
214
211
|
} catch { /* ignore */ }
|
|
@@ -223,12 +220,12 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
223
220
|
let currentToolCallId = null;
|
|
224
221
|
let currentToolName = null;
|
|
225
222
|
let currentToolInput = '';
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
223
|
+
// Accumulate raw SSE body verbatim for the debug panel. No truncation:
|
|
224
|
+
// a truncated copy of the response is misleading. If the resulting
|
|
225
|
+
// payload is too large for the debug store the bound is per-LOOP-COUNT
|
|
226
|
+
// on the client (see web/stores/chat.js MAX_UNIFY_DEBUG_LOOPS), not
|
|
227
|
+
// per-payload mutilation here.
|
|
229
228
|
let rawSseBody = '';
|
|
230
|
-
let rawSseTotalBytes = 0;
|
|
231
|
-
let rawSseCapped = false;
|
|
232
229
|
const responseHeaders = response.headers && typeof response.headers.entries === 'function'
|
|
233
230
|
? Object.fromEntries(response.headers.entries())
|
|
234
231
|
: {};
|
|
@@ -241,20 +238,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
241
238
|
|
|
242
239
|
const chunkText = decoder.decode(value, { stream: true });
|
|
243
240
|
buffer += chunkText;
|
|
244
|
-
|
|
245
|
-
rawSseTotalBytes += value.byteLength;
|
|
246
|
-
if (!rawSseCapped) {
|
|
247
|
-
if (rawSseTotalBytes <= RAW_PAYLOAD_CAP_BYTES) {
|
|
248
|
-
rawSseBody += chunkText;
|
|
249
|
-
} else {
|
|
250
|
-
// Append enough of this chunk to meet the cap, then freeze.
|
|
251
|
-
const remaining = RAW_PAYLOAD_CAP_BYTES - (rawSseTotalBytes - value.byteLength);
|
|
252
|
-
if (remaining > 0) {
|
|
253
|
-
rawSseBody += chunkText.slice(0, remaining);
|
|
254
|
-
}
|
|
255
|
-
rawSseCapped = true;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
241
|
+
rawSseBody += chunkText;
|
|
258
242
|
const lines = buffer.split('\n');
|
|
259
243
|
buffer = lines.pop() || ''; // Keep incomplete line
|
|
260
244
|
|
|
@@ -344,19 +328,16 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
344
328
|
}
|
|
345
329
|
} finally {
|
|
346
330
|
reader.releaseLock();
|
|
347
|
-
//
|
|
331
|
+
// Emit raw exchange after stream completes (or errors). Body is the
|
|
332
|
+
// verbatim SSE — never truncated.
|
|
348
333
|
if (onRawExchange) {
|
|
349
334
|
try {
|
|
350
|
-
// task-344 follow-up (N2): append truncation marker when capped.
|
|
351
|
-
const finalBody = rawSseCapped
|
|
352
|
-
? `${rawSseBody}…[truncated, original ${rawSseTotalBytes} bytes]`
|
|
353
|
-
: rawSseBody;
|
|
354
335
|
onRawExchange({
|
|
355
336
|
rawRequest,
|
|
356
337
|
rawResponse: {
|
|
357
338
|
status: responseStatus,
|
|
358
339
|
headers: responseHeaders,
|
|
359
|
-
body:
|
|
340
|
+
body: rawSseBody,
|
|
360
341
|
format: 'sse',
|
|
361
342
|
},
|
|
362
343
|
});
|