@yeaft/webchat-agent 1.0.13 → 1.0.14

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": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -2355,6 +2355,89 @@ function maybeTransitionVpStatus(hctx, state) {
2355
2355
  }
2356
2356
  }
2357
2357
 
2358
+ const STREAM_TEXT_BATCH_MAX_CHARS = 200;
2359
+ const STREAM_TEXT_BATCH_MAX_MS = 200;
2360
+
2361
+ function createStreamTextBatch() {
2362
+ return {
2363
+ parts: [],
2364
+ charCount: 0,
2365
+ timer: null,
2366
+ envelope: null,
2367
+ immediateNext: true,
2368
+ };
2369
+ }
2370
+
2371
+ function getStreamTextBatch(hctx) {
2372
+ if (!hctx) return null;
2373
+ if (!hctx.streamTextBatch) hctx.streamTextBatch = createStreamTextBatch();
2374
+ return hctx.streamTextBatch;
2375
+ }
2376
+
2377
+ function clearStreamTextBatchTimer(batch) {
2378
+ if (!batch?.timer) return;
2379
+ clearTimeout(batch.timer);
2380
+ batch.timer = null;
2381
+ }
2382
+
2383
+ function sendAssistantTextFrame(text, envelope) {
2384
+ if (!text) return;
2385
+ sendSessionOutputFrame({
2386
+ type: 'assistant',
2387
+ message: { content: [{ type: 'text', text }] },
2388
+ }, envelope);
2389
+ }
2390
+
2391
+ function flushStreamTextBatch(hctx, envelope, { resetImmediate = false } = {}) {
2392
+ const batch = hctx?.streamTextBatch;
2393
+ if (!batch) return false;
2394
+ clearStreamTextBatchTimer(batch);
2395
+ const text = batch.parts.join('');
2396
+ batch.parts = [];
2397
+ batch.charCount = 0;
2398
+ const flushEnvelope = envelope || batch.envelope;
2399
+ batch.envelope = flushEnvelope || null;
2400
+ if (resetImmediate) batch.immediateNext = true;
2401
+ if (!text) return false;
2402
+ sendAssistantTextFrame(text, flushEnvelope);
2403
+ return true;
2404
+ }
2405
+
2406
+ function scheduleStreamTextBatchFlush(hctx, batch) {
2407
+ if (!hctx || batch.timer) return;
2408
+ batch.timer = setTimeout(() => {
2409
+ batch.timer = null;
2410
+ flushStreamTextBatch(hctx, batch.envelope);
2411
+ }, STREAM_TEXT_BATCH_MAX_MS);
2412
+ if (batch.timer && typeof batch.timer.unref === 'function') {
2413
+ batch.timer.unref();
2414
+ }
2415
+ }
2416
+
2417
+ function queueStreamTextDelta(hctx, text, envelope) {
2418
+ if (typeof text !== 'string' || text.length === 0) return;
2419
+ const batch = getStreamTextBatch(hctx);
2420
+ if (!batch) {
2421
+ sendAssistantTextFrame(text, envelope);
2422
+ return;
2423
+ }
2424
+
2425
+ batch.envelope = envelope;
2426
+ if (batch.immediateNext) {
2427
+ batch.immediateNext = false;
2428
+ sendAssistantTextFrame(text, envelope);
2429
+ return;
2430
+ }
2431
+
2432
+ batch.parts.push(text);
2433
+ batch.charCount += text.length;
2434
+ if (batch.charCount >= STREAM_TEXT_BATCH_MAX_CHARS) {
2435
+ flushStreamTextBatch(hctx, envelope);
2436
+ return;
2437
+ }
2438
+ scheduleStreamTextBatchFlush(hctx, batch);
2439
+ }
2440
+
2358
2441
  /**
2359
2442
  * Handle a single engine event unwrapped from an `engine_event` envelope.
2360
2443
  * Stamps threadId on every outgoing frame so frontend grouping, tools,
@@ -2376,13 +2459,17 @@ function handleEngineEvent(event, hctx) {
2376
2459
  threadId: hctx.threadId || event.threadId,
2377
2460
  };
2378
2461
 
2462
+ if (event.type !== 'text_delta') {
2463
+ // Preserve wire order. Any boundary/metadata/tool event must see all text
2464
+ // accepted before it flushed first; otherwise the browser can render a tool
2465
+ // call or terminal result before the text that led to it.
2466
+ flushStreamTextBatch(hctx, envelope, { resetImmediate: true });
2467
+ }
2468
+
2379
2469
  switch (event.type) {
2380
2470
  case 'text_delta':
2381
2471
  hctx.assistantTextParts.push(event.text);
2382
- sendSessionOutputFrame({
2383
- type: 'assistant',
2384
- message: { content: [{ type: 'text', text: event.text }] },
2385
- }, envelope);
2472
+ queueStreamTextDelta(hctx, event.text, envelope);
2386
2473
  // vp-status: first text-delta of a (thinking|tool) phase flips
2387
2474
  // the row to 'streaming'. transition() is a no-op when already
2388
2475
  // streaming, so subsequent deltas are cheap.
@@ -3411,6 +3498,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3411
3498
  let turnEndReason = 'end_turn';
3412
3499
  let turnEndEmitted = false;
3413
3500
  let turnEndDetail = null;
3501
+ let handlerCtx = null;
3414
3502
  const markTurnEnd = (reason) => { turnEndEmitted = true; turnEndReason = reason; };
3415
3503
  const emitVpTurnEnd = (reason, detail = null) => {
3416
3504
  if (turnEndEmitted) return;
@@ -3484,7 +3572,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3484
3572
  vpEngine = getOrCreateVpEngine(sessionId, vpId, threadId);
3485
3573
  if (thread) thread.engine = vpEngine;
3486
3574
 
3487
- const handlerCtx = {
3575
+ handlerCtx = {
3488
3576
  assistantTextParts,
3489
3577
  toolCallsAccum,
3490
3578
  toolResultsAccum,
@@ -3530,6 +3618,8 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3530
3618
  handleEngineEvent(event, handlerCtx);
3531
3619
  }
3532
3620
 
3621
+ flushStreamTextBatch(handlerCtx, envelope, { resetImmediate: true });
3622
+
3533
3623
  // Turn completed — atomically append this VP's output to shared history.
3534
3624
  // route_forward handoff text is an internal trigger, already visible as
3535
3625
  // the source VP's tool action. Do not append it as a visible prompt for
@@ -3558,6 +3648,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3558
3648
  } catch (err) {
3559
3649
  const isAbort = err && (err.name === 'AbortError' || err.name === 'LLMAbortError');
3560
3650
  if (isAbort) {
3651
+ flushStreamTextBatch(handlerCtx, envelope, { resetImmediate: true });
3561
3652
  sendSessionOutputFrame({
3562
3653
  type: 'result',
3563
3654
  result_text: '',
@@ -3582,6 +3673,8 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3582
3673
  console.warn('[Yeaft] vp-status error transition failed:', brokerErr?.message || brokerErr);
3583
3674
  }
3584
3675
 
3676
+ flushStreamTextBatch(handlerCtx, envelope, { resetImmediate: true });
3677
+
3585
3678
  if (isPermissionErrorMsg(err.message)) {
3586
3679
  if (!_permissionDiagnosticSent) {
3587
3680
  _permissionDiagnosticSent = true;