@yeaft/webchat-agent 0.1.562 → 0.1.564

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.562",
3
+ "version": "0.1.564",
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
@@ -661,6 +661,13 @@ export class Engine {
661
661
  const toolCalls = [];
662
662
  let stopReason = 'end_turn';
663
663
  const totalUsage = { inputTokens: 0, outputTokens: 0 };
664
+ // task-344: capture redacted raw request / raw response for debug panel.
665
+ let rawRequest = null;
666
+ let rawResponse = null;
667
+ const captureRawExchange = (exchange) => {
668
+ if (exchange?.rawRequest) rawRequest = exchange.rawRequest;
669
+ if (exchange?.rawResponse) rawResponse = exchange.rawResponse;
670
+ };
664
671
 
665
672
  yield { type: 'turn_start', turnNumber };
666
673
 
@@ -678,6 +685,7 @@ export class Engine {
678
685
  maxTokens: this.#config.maxOutputTokens || 16384,
679
686
  effort: resolvedEffort,
680
687
  signal,
688
+ onRawExchange: captureRawExchange,
681
689
  })) {
682
690
  switch (event.type) {
683
691
  case 'text_delta':
@@ -730,6 +738,8 @@ export class Engine {
730
738
  latencyMs,
731
739
  ttfbMs,
732
740
  stopReason: 'error',
741
+ rawRequest,
742
+ rawResponse,
733
743
  };
734
744
 
735
745
  // ─── task-325a: abort short-circuit ────────────────
@@ -806,6 +816,8 @@ export class Engine {
806
816
  latencyMs,
807
817
  ttfbMs,
808
818
  stopReason,
819
+ rawRequest,
820
+ rawResponse,
809
821
  };
810
822
 
811
823
  // Append assistant message to conversation
@@ -101,6 +101,29 @@ export class LLMAbortError extends Error {
101
101
  }
102
102
  }
103
103
 
104
+ // ─── task-344: Raw payload redaction helper ────────────────────
105
+
106
+ /**
107
+ * Redact sensitive headers (API keys / bearer tokens) from a raw request
108
+ * shape before exposing it to debug UI. Always returns a NEW object — never
109
+ * mutates the input.
110
+ *
111
+ * @param {{ url: string, method: string, headers: object, body: any }} req
112
+ * @returns {{ url: string, method: string, headers: object, body: any }}
113
+ */
114
+ export function redactRawRequest(req) {
115
+ if (!req || typeof req !== 'object') return req;
116
+ const headers = { ...(req.headers || {}) };
117
+ // Common auth headers
118
+ for (const k of Object.keys(headers)) {
119
+ const lower = k.toLowerCase();
120
+ if (lower === 'x-api-key' || lower === 'authorization' || lower === 'api-key') {
121
+ headers[k] = '***';
122
+ }
123
+ }
124
+ return { url: req.url, method: req.method, headers, body: req.body };
125
+ }
126
+
104
127
  // ─── Base Class ────────────────────────────────────────────────
105
128
 
106
129
  /**
@@ -13,6 +13,7 @@ import {
13
13
  LLMContextError,
14
14
  LLMServerError,
15
15
  LLMAbortError,
16
+ redactRawRequest,
16
17
  } from './adapter.js';
17
18
  import {
18
19
  normalizeEffort,
@@ -139,7 +140,7 @@ export class AnthropicAdapter extends LLMAdapter {
139
140
  * @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', signal?: AbortSignal }} params
140
141
  * @returns {AsyncGenerator<import('./adapter.js').StreamEvent>}
141
142
  */
142
- async *stream({ model, system, messages, tools, maxTokens = 16384, effort, signal }) {
143
+ async *stream({ model, system, messages, tools, maxTokens = 16384, effort, signal, onRawExchange }) {
143
144
  if (signal?.aborted) throw new LLMAbortError();
144
145
 
145
146
  const body = {
@@ -173,19 +174,40 @@ export class AnthropicAdapter extends LLMAdapter {
173
174
  const translatedTools = this.#translateTools(tools);
174
175
  if (translatedTools) body.tools = translatedTools;
175
176
 
176
- const response = await fetch(`${this.#baseUrl}/v1/messages`, {
177
+ const url = `${this.#baseUrl}/v1/messages`;
178
+ const headers = {
179
+ 'Content-Type': 'application/json',
180
+ 'x-api-key': this.#apiKey,
181
+ 'anthropic-version': API_VERSION,
182
+ };
183
+
184
+ // task-344: expose raw request (redacted) for debug panel.
185
+ const rawRequest = redactRawRequest({ url, method: 'POST', headers, body });
186
+
187
+ const response = await fetch(url, {
177
188
  method: 'POST',
178
- headers: {
179
- 'Content-Type': 'application/json',
180
- 'x-api-key': this.#apiKey,
181
- 'anthropic-version': API_VERSION,
182
- },
189
+ headers,
183
190
  body: JSON.stringify(body),
184
191
  signal,
185
192
  });
186
193
 
187
194
  if (!response.ok) {
188
195
  const errorBody = await response.text();
196
+ // task-344: capture error response too, then throw.
197
+ if (onRawExchange) {
198
+ try {
199
+ onRawExchange({
200
+ rawRequest,
201
+ rawResponse: {
202
+ status: response.status,
203
+ headers: response.headers && typeof response.headers.entries === 'function'
204
+ ? Object.fromEntries(response.headers.entries())
205
+ : {},
206
+ body: errorBody,
207
+ },
208
+ });
209
+ } catch { /* ignore */ }
210
+ }
189
211
  throw this.#classifyError(response.status, errorBody);
190
212
  }
191
213
 
@@ -196,13 +218,21 @@ export class AnthropicAdapter extends LLMAdapter {
196
218
  let currentToolCallId = null;
197
219
  let currentToolName = null;
198
220
  let currentToolInput = '';
221
+ // task-344: accumulate raw SSE body for debug exposure.
222
+ let rawSseBody = '';
223
+ const responseHeaders = response.headers && typeof response.headers.entries === 'function'
224
+ ? Object.fromEntries(response.headers.entries())
225
+ : {};
226
+ const responseStatus = response.status;
199
227
 
200
228
  try {
201
229
  while (true) {
202
230
  const { done, value } = await reader.read();
203
231
  if (done) break;
204
232
 
205
- buffer += decoder.decode(value, { stream: true });
233
+ const chunkText = decoder.decode(value, { stream: true });
234
+ buffer += chunkText;
235
+ rawSseBody += chunkText;
206
236
  const lines = buffer.split('\n');
207
237
  buffer = lines.pop() || ''; // Keep incomplete line
208
238
 
@@ -292,6 +322,20 @@ export class AnthropicAdapter extends LLMAdapter {
292
322
  }
293
323
  } finally {
294
324
  reader.releaseLock();
325
+ // task-344: emit raw exchange after stream completes (or errors).
326
+ if (onRawExchange) {
327
+ try {
328
+ onRawExchange({
329
+ rawRequest,
330
+ rawResponse: {
331
+ status: responseStatus,
332
+ headers: responseHeaders,
333
+ body: rawSseBody,
334
+ format: 'sse',
335
+ },
336
+ });
337
+ } catch { /* ignore */ }
338
+ }
295
339
  }
296
340
  }
297
341
 
@@ -27,6 +27,7 @@ import {
27
27
  LLMContextError,
28
28
  LLMServerError,
29
29
  LLMAbortError,
30
+ redactRawRequest,
30
31
  } from './adapter.js';
31
32
  import {
32
33
  normalizeEffort,
@@ -199,7 +200,7 @@ export class ChatCompletionsAdapter extends LLMAdapter {
199
200
  * @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
200
201
  * @returns {AsyncGenerator<import('./adapter.js').StreamEvent>}
201
202
  */
202
- async *stream({ model, system, messages, tools, maxTokens = 16384, effort, extraBody, signal }) {
203
+ async *stream({ model, system, messages, tools, maxTokens = 16384, effort, extraBody, signal, onRawExchange }) {
203
204
  if (signal?.aborted) throw new LLMAbortError();
204
205
 
205
206
  const body = {
@@ -231,18 +232,38 @@ export class ChatCompletionsAdapter extends LLMAdapter {
231
232
  // extraBody allows callers to pass through any additional/override parameters
232
233
  if (extraBody) Object.assign(body, extraBody);
233
234
 
234
- const response = await fetch(`${this.#baseUrl}/chat/completions`, {
235
+ const url = `${this.#baseUrl}/chat/completions`;
236
+ const headers = {
237
+ 'Content-Type': 'application/json',
238
+ 'Authorization': `Bearer ${this.#apiKey}`,
239
+ };
240
+
241
+ // task-344: expose raw request (redacted) for debug panel.
242
+ const rawRequest = redactRawRequest({ url, method: 'POST', headers, body });
243
+
244
+ const response = await fetch(url, {
235
245
  method: 'POST',
236
- headers: {
237
- 'Content-Type': 'application/json',
238
- 'Authorization': `Bearer ${this.#apiKey}`,
239
- },
246
+ headers,
240
247
  body: JSON.stringify(body),
241
248
  signal,
242
249
  });
243
250
 
244
251
  if (!response.ok) {
245
252
  const errorBody = await response.text();
253
+ if (onRawExchange) {
254
+ try {
255
+ onRawExchange({
256
+ rawRequest,
257
+ rawResponse: {
258
+ status: response.status,
259
+ headers: response.headers && typeof response.headers.entries === 'function'
260
+ ? Object.fromEntries(response.headers.entries())
261
+ : {},
262
+ body: errorBody,
263
+ },
264
+ });
265
+ } catch { /* ignore */ }
266
+ }
246
267
  throw this.#classifyError(response.status, errorBody);
247
268
  }
248
269
 
@@ -250,6 +271,12 @@ export class ChatCompletionsAdapter extends LLMAdapter {
250
271
  const reader = response.body.getReader();
251
272
  const decoder = new TextDecoder();
252
273
  let buffer = '';
274
+ // task-344: accumulate raw SSE body + headers/status for debug exposure.
275
+ let rawSseBody = '';
276
+ const responseHeaders = response.headers && typeof response.headers.entries === 'function'
277
+ ? Object.fromEntries(response.headers.entries())
278
+ : {};
279
+ const responseStatus = response.status;
253
280
 
254
281
  // Tool call accumulation — Chat Completions sends tool args as fragments
255
282
  // keyed by index within the delta.tool_calls array
@@ -261,7 +288,9 @@ export class ChatCompletionsAdapter extends LLMAdapter {
261
288
  const { done, value } = await reader.read();
262
289
  if (done) break;
263
290
 
264
- buffer += decoder.decode(value, { stream: true });
291
+ const chunkText = decoder.decode(value, { stream: true });
292
+ buffer += chunkText;
293
+ rawSseBody += chunkText;
265
294
  const lines = buffer.split('\n');
266
295
  buffer = lines.pop() || '';
267
296
 
@@ -345,6 +374,20 @@ export class ChatCompletionsAdapter extends LLMAdapter {
345
374
  }
346
375
  } finally {
347
376
  reader.releaseLock();
377
+ // task-344: emit raw exchange after stream completes.
378
+ if (onRawExchange) {
379
+ try {
380
+ onRawExchange({
381
+ rawRequest,
382
+ rawResponse: {
383
+ status: responseStatus,
384
+ headers: responseHeaders,
385
+ body: rawSseBody,
386
+ format: 'sse',
387
+ },
388
+ });
389
+ } catch { /* ignore */ }
390
+ }
348
391
  }
349
392
  }
350
393
 
@@ -898,6 +898,9 @@ function handleEngineEvent(event, threadId, hctx) {
898
898
  latencyMs: event.latencyMs,
899
899
  ttfbMs: event.ttfbMs,
900
900
  stopReason: event.stopReason,
901
+ // task-344: forward raw request / response (redacted) to web debug panel.
902
+ rawRequest: event.rawRequest,
903
+ rawResponse: event.rawResponse,
901
904
  threadId,
902
905
  });
903
906
  break;