@yeaft/webchat-agent 0.1.701 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.701",
3
+ "version": "0.1.703",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
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 truncated at 50000 chars; each tool_call input is JSON-stringified
78
- * + sliced at 10000 chars before being re-parsed, so a runaway `input` blob
79
- * can't blow past the WebSocket frame budget. Unknown roles pass through
80
- * unchanged.
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 = typeof m.content === 'string' ? m.content.slice(0, 50000) : m.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
- let input = tc.input;
93
- try {
94
- const s = JSON.stringify(input);
95
- if (typeof s === 'string' && s.length > 10000) {
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;
@@ -105,65 +105,20 @@ export class LLMAbortError extends Error {
105
105
  }
106
106
  }
107
107
 
108
- // ─── task-344: Raw payload redaction helper ────────────────────
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
  */
@@ -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
- // task-344: expose raw request (redacted) for debug panel.
188
- // task-344 follow-up (N2): cap body to RAW_PAYLOAD_CAP_BYTES.
189
- const rawRequest = capRawRequest(redactRawRequest({ url, method: 'POST', headers, body }));
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
- // task-344: capture error response too, then throw.
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
- // task-344 follow-up (N2): cap error body.
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
- // task-344: accumulate raw SSE body for debug exposure.
227
- // task-344 follow-up (N2): cap growth once past RAW_PAYLOAD_CAP_BYTES
228
- // we freeze the captured body and record total-bytes-seen separately.
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
- // task-344 follow-up (N2): size-capped capture.
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
- // task-344: emit raw exchange after stream completes (or errors).
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: finalBody,
340
+ body: rawSseBody,
360
341
  format: 'sse',
361
342
  },
362
343
  });