@yeaft/webchat-agent 0.1.703 → 0.1.704

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.703",
3
+ "version": "0.1.704",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -135,6 +135,27 @@ export function redactRawRequest(req) {
135
135
  return { url: req.url, method: req.method, headers, body: req.body };
136
136
  }
137
137
 
138
+ /**
139
+ * Snapshot a Fetch Response's headers into a plain object for the debug
140
+ * panel. Defensive against polyfilled / mocked Response shapes that don't
141
+ * implement `Headers#entries()` — falls back to `{}` rather than throwing.
142
+ *
143
+ * NOTE: multi-valued headers (e.g. `Set-Cookie`) collapse to the last value
144
+ * because `Object.fromEntries` can't represent duplicates. For LLM debug
145
+ * traffic this is fine; if a future use case needs multi-valued capture,
146
+ * switch the return to an array of [k, v] pairs.
147
+ *
148
+ * @param {Response | { headers?: { entries?: () => Iterable<[string, string]> } }} response
149
+ * @returns {Record<string, string>}
150
+ */
151
+ export function safeHeaders(response) {
152
+ const h = response && response.headers;
153
+ if (h && typeof h.entries === 'function') {
154
+ return Object.fromEntries(h.entries());
155
+ }
156
+ return {};
157
+ }
158
+
138
159
  // ─── Base Class ────────────────────────────────────────────────
139
160
 
140
161
  /**
@@ -14,6 +14,7 @@ import {
14
14
  LLMServerError,
15
15
  LLMAbortError,
16
16
  redactRawRequest,
17
+ safeHeaders,
17
18
  } from './adapter.js';
18
19
  import {
19
20
  normalizeEffort,
@@ -202,9 +203,7 @@ export class AnthropicAdapter extends LLMAdapter {
202
203
  rawRequest,
203
204
  rawResponse: {
204
205
  status: response.status,
205
- headers: response.headers && typeof response.headers.entries === 'function'
206
- ? Object.fromEntries(response.headers.entries())
207
- : {},
206
+ headers: safeHeaders(response),
208
207
  body: errorBody,
209
208
  },
210
209
  });
@@ -221,14 +220,11 @@ export class AnthropicAdapter extends LLMAdapter {
221
220
  let currentToolName = null;
222
221
  let currentToolInput = '';
223
222
  // 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.
228
- let rawSseBody = '';
229
- const responseHeaders = response.headers && typeof response.headers.entries === 'function'
230
- ? Object.fromEntries(response.headers.entries())
231
- : {};
223
+ // see `redactRawRequest` in adapter.js for the verbatim-design rationale.
224
+ // Push-then-join keeps allocation bounded for multi-MiB payloads (avoids
225
+ // O(n²) string concat).
226
+ const rawSseBodyChunks = [];
227
+ const responseHeaders = safeHeaders(response);
232
228
  const responseStatus = response.status;
233
229
 
234
230
  try {
@@ -238,7 +234,7 @@ export class AnthropicAdapter extends LLMAdapter {
238
234
 
239
235
  const chunkText = decoder.decode(value, { stream: true });
240
236
  buffer += chunkText;
241
- rawSseBody += chunkText;
237
+ rawSseBodyChunks.push(chunkText);
242
238
  const lines = buffer.split('\n');
243
239
  buffer = lines.pop() || ''; // Keep incomplete line
244
240
 
@@ -337,7 +333,7 @@ export class AnthropicAdapter extends LLMAdapter {
337
333
  rawResponse: {
338
334
  status: responseStatus,
339
335
  headers: responseHeaders,
340
- body: rawSseBody,
336
+ body: rawSseBodyChunks.join(''),
341
337
  format: 'sse',
342
338
  },
343
339
  });
@@ -32,6 +32,8 @@ import {
32
32
  LLMContextError,
33
33
  LLMServerError,
34
34
  LLMAbortError,
35
+ redactRawRequest,
36
+ safeHeaders,
35
37
  } from './adapter.js';
36
38
  import {
37
39
  normalizeEffort,
@@ -225,9 +227,16 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
225
227
  // ─── Streaming ──────────────────────────────────────────
226
228
 
227
229
  /**
228
- * @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', extraBody?: object, signal?: AbortSignal }} params
230
+ * @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', extraBody?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
231
+ *
232
+ * NOTE on `extraBody`: any keys you spread here are merged verbatim into
233
+ * the wire body and — because the verbatim debug feature is intentionally
234
+ * non-truncating — will surface in the debug panel via `rawRequest.body`.
235
+ * Do NOT put secrets in `extraBody`. Only `Authorization` / `x-api-key` /
236
+ * `api-key` headers are auto-redacted (see `redactRawRequest` in
237
+ * `adapter.js`); request-body fields are caller-controlled.
229
238
  */
230
- async *stream({ model, system, messages, tools, maxTokens = 16384, effort, extraBody, signal }) {
239
+ async *stream({ model, system, messages, tools, maxTokens = 16384, effort, extraBody, signal, onRawExchange }) {
231
240
  if (signal?.aborted) throw new LLMAbortError();
232
241
 
233
242
  const body = {
@@ -255,14 +264,21 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
255
264
 
256
265
  if (extraBody) Object.assign(body, extraBody);
257
266
 
267
+ const url = `${this.#baseUrl}/responses`;
268
+ const headers = {
269
+ 'Content-Type': 'application/json',
270
+ 'Authorization': `Bearer ${this.#apiKey}`,
271
+ };
272
+
273
+ // Expose the raw request (auth-redacted) for the debug panel. See
274
+ // `redactRawRequest` in adapter.js for the verbatim-design rationale.
275
+ const rawRequest = redactRawRequest({ url, method: 'POST', headers, body });
276
+
258
277
  let response;
259
278
  try {
260
- response = await fetch(`${this.#baseUrl}/responses`, {
279
+ response = await fetch(url, {
261
280
  method: 'POST',
262
- headers: {
263
- 'Content-Type': 'application/json',
264
- 'Authorization': `Bearer ${this.#apiKey}`,
265
- },
281
+ headers,
266
282
  body: JSON.stringify(body),
267
283
  signal,
268
284
  });
@@ -273,6 +289,19 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
273
289
 
274
290
  if (!response.ok) {
275
291
  const errorBody = await response.text();
292
+ // Capture error response too, then throw. Parity with anthropic.js.
293
+ if (onRawExchange) {
294
+ try {
295
+ onRawExchange({
296
+ rawRequest,
297
+ rawResponse: {
298
+ status: response.status,
299
+ headers: safeHeaders(response),
300
+ body: errorBody,
301
+ },
302
+ });
303
+ } catch { /* ignore */ }
304
+ }
276
305
  throw this.#classifyError(response.status, errorBody);
277
306
  }
278
307
 
@@ -287,11 +316,21 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
287
316
  const emittedToolCallIds = new Set();
288
317
  let sawToolCall = false;
289
318
 
319
+ // Accumulate raw SSE body verbatim for the debug panel. No truncation:
320
+ // see `redactRawRequest` in adapter.js for the verbatim-design rationale.
321
+ // Push-then-join keeps allocation bounded for multi-MiB payloads (avoids
322
+ // O(n²) string concat).
323
+ const rawSseBodyChunks = [];
324
+ const responseHeaders = safeHeaders(response);
325
+ const responseStatus = response.status;
326
+
290
327
  try {
291
328
  while (true) {
292
329
  const { done, value } = await reader.read();
293
330
  if (done) break;
294
- buffer += decoder.decode(value, { stream: true });
331
+ const chunkText = decoder.decode(value, { stream: true });
332
+ buffer += chunkText;
333
+ rawSseBodyChunks.push(chunkText);
295
334
 
296
335
  // SSE events are separated by blank lines; split on \n
297
336
  const lines = buffer.split('\n');
@@ -409,11 +448,33 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
409
448
  throw err;
410
449
  } finally {
411
450
  try { reader.releaseLock(); } catch { /* noop */ }
451
+ // Emit raw exchange after stream completes (or errors). Body is the
452
+ // verbatim SSE — never truncated. Parity with anthropic.js.
453
+ if (onRawExchange) {
454
+ try {
455
+ onRawExchange({
456
+ rawRequest,
457
+ rawResponse: {
458
+ status: responseStatus,
459
+ headers: responseHeaders,
460
+ body: rawSseBodyChunks.join(''),
461
+ format: 'sse',
462
+ },
463
+ });
464
+ } catch { /* ignore */ }
465
+ }
412
466
  }
413
467
  }
414
468
 
415
469
  // ─── Non-streaming call() ───────────────────────────────
416
470
 
471
+ /**
472
+ * Side-query (consolidate / dream / recall / light) entry point. Does
473
+ * NOT accept `onRawExchange` — these calls intentionally don't surface
474
+ * in the user-facing debug panel. If a future product change wants to
475
+ * expose them, mirror the stream() instrumentation. Parity with
476
+ * anthropic.js's `call()`.
477
+ */
417
478
  async call({ model, system, messages, maxTokens = 4096, effort, extraBody, signal }) {
418
479
  if (signal?.aborted) throw new LLMAbortError();
419
480