@yeaft/webchat-agent 1.0.576 → 1.0.578

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.
@@ -1 +1 @@
1
- {"version":"1.0.576"}
1
+ {"version":"1.0.578"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.576",
3
+ "version": "1.0.578",
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",
package/yeaft/engine.js CHANGED
@@ -3169,7 +3169,12 @@ export class Engine {
3169
3169
  });
3170
3170
  }
3171
3171
  switch (event.type) {
3172
+ case 'provider_activity':
3173
+ // Internal liveness only: no text, tools, usage or durable state.
3174
+ if (!sawProviderStop) yield { type: 'provider_activity' };
3175
+ break;
3172
3176
  case 'text_delta':
3177
+ if (typeof event.text !== 'string' || event.text.length === 0) break;
3173
3178
  if (ttfbMs === null) {
3174
3179
  ttfbMs = Date.now() - startTime;
3175
3180
  traceRequest('llm.first_text', {
@@ -3184,6 +3189,7 @@ export class Engine {
3184
3189
  providerState = event.providerState;
3185
3190
  break;
3186
3191
  case 'thinking_delta':
3192
+ if (typeof event.text !== 'string' || event.text.length === 0) break;
3187
3193
  yield event;
3188
3194
  break;
3189
3195
  case 'thinking_block_end':
@@ -44,12 +44,13 @@ import { utf8PrefixWithinBytes } from '../utf8.js';
44
44
  * @typedef {{ type: 'thinking_delta', text: string }} ThinkingDeltaEvent
45
45
  * @typedef {{ type: 'thinking_block_end', thinking: string, signature: string }} ThinkingBlockEndEvent
46
46
  * @typedef {{ type: 'tool_call', id: string, name: string, input: object }} ToolCallEvent
47
+ * @typedef {{ type: 'provider_activity' }} ProviderActivityEvent Content-free progress on a non-empty hidden delta; watchdog only, never UI/history.
47
48
  * @typedef {{ type: 'provider_state', providerState: object, providerStateBytes: number }} ProviderStateEvent Internal only; never forward to UI/search.
48
49
  * @typedef {{ type: 'usage', inputTokens: number, outputTokens: number, reasoningTokens?: number, cacheReadTokens?: number, cacheWriteTokens?: number, cacheTokensAreIncludedInInput?: boolean }} UsageEvent
49
50
  * @typedef {{ type: 'stop', stopReason: 'end_turn' | 'tool_use' | 'max_tokens' }} StopEvent
50
51
  * @typedef {{ type: 'error', error: Error, retryable: boolean }} ErrorEvent
51
52
  *
52
- * @typedef {TextDeltaEvent | ThinkingDeltaEvent | ThinkingBlockEndEvent | ProviderStateEvent | ToolCallEvent | UsageEvent | StopEvent | ErrorEvent} StreamEvent
53
+ * @typedef {TextDeltaEvent | ThinkingDeltaEvent | ThinkingBlockEndEvent | ProviderActivityEvent | ProviderStateEvent | ToolCallEvent | UsageEvent | StopEvent | ErrorEvent} StreamEvent
53
54
  */
54
55
 
55
56
  // ─── Unified Message Types ─────────────────────────────────────
@@ -395,6 +395,8 @@ export class AnthropicAdapter extends LLMAdapter {
395
395
  }
396
396
 
397
397
  const type = event.type;
398
+ if (signal?.aborted) throw new LLMAbortError();
399
+ if (sawStop) continue;
398
400
 
399
401
  if (type === 'content_block_start') {
400
402
  const block = event.content_block;
@@ -435,11 +437,15 @@ export class AnthropicAdapter extends LLMAdapter {
435
437
  const st = blockByIndex.get(idx);
436
438
  if (delta?.type === 'text_delta') {
437
439
  if (st?.kind === 'text') st.text += delta.text || '';
438
- yield { type: 'text_delta', text: delta.text };
440
+ if (typeof delta.text === 'string' && delta.text.length > 0) {
441
+ yield { type: 'text_delta', text: delta.text };
442
+ }
439
443
  } else if (delta?.type === 'thinking_delta') {
440
444
  // Forward delta for live UI; ALSO accumulate for round-trip.
441
445
  if (st && st.kind === 'thinking') st.thinking += delta.thinking || '';
442
- yield { type: 'thinking_delta', text: delta.thinking };
446
+ if (typeof delta.thinking === 'string' && delta.thinking.length > 0) {
447
+ yield { type: 'thinking_delta', text: delta.thinking };
448
+ }
443
449
  } else if (delta?.type === 'signature_delta') {
444
450
  // Anthropic typically sends signature in one delta near the
445
451
  // end of the (redacted_)thinking block. Accumulate defensively.
@@ -447,7 +453,11 @@ export class AnthropicAdapter extends LLMAdapter {
447
453
  st.signature += delta.signature || '';
448
454
  }
449
455
  } else if (delta?.type === 'input_json_delta') {
450
- if (st && st.kind === 'tool_use') st.input += delta.partial_json;
456
+ if (st?.kind === 'tool_use' && typeof delta.partial_json === 'string' && delta.partial_json.length > 0) {
457
+ st.input += delta.partial_json;
458
+ // No content and no throttling: the last delta defines idle time.
459
+ yield { type: 'provider_activity' };
460
+ }
451
461
  }
452
462
  } else if (type === 'content_block_stop') {
453
463
  const idx = event.index;
@@ -429,6 +429,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
429
429
 
430
430
  const type = event.type;
431
431
 
432
+ if (signal?.aborted) throw new LLMAbortError();
432
433
  if (sawTerminalEvent) continue;
433
434
  if (type === 'response.output_item.done') {
434
435
  if (Number.isInteger(event.output_index) && event.item) completedItems.set(event.output_index, event.item);
@@ -450,8 +451,14 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
450
451
  } else if (type === 'response.function_call_arguments.delta') {
451
452
  const idx = event.output_index;
452
453
  const accum = toolCallAccum.get(idx);
453
- if (accum) {
454
- accum.arguments += event.delta || '';
454
+ if (accum && typeof event.delta === 'string' && event.delta.length > 0) {
455
+ accum.arguments += event.delta;
456
+ yield { type: 'provider_activity' };
457
+ }
458
+ } else if (type === 'response.reasoning_text.delta' || type === 'response.reasoning_summary_text.delta') {
459
+ // Reasoning remains hidden; only report actual incremental content.
460
+ if (typeof event.delta === 'string' && event.delta.length > 0) {
461
+ yield { type: 'provider_activity' };
455
462
  }
456
463
  } else if (type === 'response.function_call_arguments.done') {
457
464
  const idx = event.output_index;
package/yeaft/models.js CHANGED
@@ -496,8 +496,9 @@ function inferThinkingCapability(model) {
496
496
  return { supportsThinking: true, thinkingProtocol: 'openai-reasoning', defaultEffort: null, maxBudgetTokens: null };
497
497
  }
498
498
 
499
- // Claude Opus 4.7/4.8 expose the full adaptive effort set, including xhigh.
500
- if (/^claude-opus-4[-.]?(7|8)($|-|\.)/.test(id)) {
499
+ // Claude Opus 4.7/4.8 and every Claude 5+ generation (e.g. claude-opus-5.5,
500
+ // claude-sonnet-5) use adaptive thinking with output_config.effort.
501
+ if (/^claude-opus-4[-.]?(7|8)($|-|\.)/.test(id) || /^claude-(opus|sonnet|haiku)-([5-9]|[1-9]\d)($|-|\.)/.test(id)) {
501
502
  return {
502
503
  supportsThinking: true,
503
504
  thinkingProtocol: 'anthropic-adaptive',
@@ -490,6 +490,8 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
490
490
  // Liveness — update first so even listener throws don't lose
491
491
  // the bump.
492
492
  bumpLivenessFromEvent(agent.liveness, evt);
493
+ // Hidden provider progress is liveness only, never a log/task/UI event.
494
+ if (evt?.type === 'provider_activity') continue;
493
495
 
494
496
  // Mirror every raw event to the durable log. We still keep
495
497
  // `sub_agent_event` text_delta suppressed so the inline transcript
@@ -1157,7 +1157,7 @@ export function broadcastLanguageChange(language) {
1157
1157
  applyLiveLanguage(language);
1158
1158
  }
1159
1159
 
1160
- /** Query timeout in ms — abort if LLM doesn't respond within this window */
1160
+ /** Provider inactivity windows (not a total-duration limit on an LLM call). */
1161
1161
  const QUERY_TIMEOUT_MS = 120_000;
1162
1162
  const HIGH_REASONING_QUERY_TIMEOUT_MS = 300_000;
1163
1163
  /** AskUser is human-paced but must never pin a VP turn forever. */
@@ -1168,6 +1168,41 @@ function isHighReasoningEffort(effort) {
1168
1168
  return value === 'high' || value === 'xhigh' || value === 'max' || value === 'ultra';
1169
1169
  }
1170
1170
 
1171
+ /**
1172
+ * Per-query provider inactivity window, not a total LLM call deadline.
1173
+ * Hidden deltas only refresh an armed timer; tool/retry waits stay paused.
1174
+ * stop() is irreversible, including on abort, so late events cannot revive it.
1175
+ * @param {{ signal: AbortSignal, timeoutMs: number, onTimeout: Function }} options
1176
+ */
1177
+ function createQueryWatchdog({ signal, timeoutMs, onTimeout }) {
1178
+ let timer = null;
1179
+ let stopped = signal.aborted;
1180
+ const pause = () => {
1181
+ if (timer) clearTimeout(timer);
1182
+ timer = null;
1183
+ };
1184
+ const stop = () => {
1185
+ stopped = true;
1186
+ pause();
1187
+ signal.removeEventListener('abort', stop);
1188
+ };
1189
+ const reset = () => {
1190
+ if (stopped || signal.aborted) return;
1191
+ if (timer) {
1192
+ timer.refresh();
1193
+ return;
1194
+ }
1195
+ timer = setTimeout(() => {
1196
+ stop();
1197
+ onTimeout();
1198
+ }, timeoutMs);
1199
+ };
1200
+ // No throttling or trailing heartbeat: the actual last delta owns idle time.
1201
+ const touch = () => { if (timer) reset(); };
1202
+ if (!stopped) signal.addEventListener('abort', stop, { once: true });
1203
+ return { reset, pause, touch, stop };
1204
+ }
1205
+
1171
1206
  function queryTimeoutMsForSessionConfig(config = null) {
1172
1207
  return isHighReasoningEffort(config?.modelEffort) ? HIGH_REASONING_QUERY_TIMEOUT_MS : QUERY_TIMEOUT_MS;
1173
1208
  }
@@ -4087,15 +4122,25 @@ function queueStreamTextDelta(hctx, text, envelope) {
4087
4122
  * todos, debug cards, and persistence all share the same boundary.
4088
4123
  *
4089
4124
  * @param {object} event — engine event (text_delta / tool_call / …)
4090
- * @param {{assistantTextParts:string[], toolCallsAccum:Array, toolResultsAccum:Array, thinkingBlocksAccum?:Array, resetQueryTimer:Function, pauseQueryTimer?:Function, markEngineTerminal?:Function, sessionId?:string, vpId?:string, turnId?:string}} hctx
4125
+ * @param {{assistantTextParts:string[], toolCallsAccum:Array, toolResultsAccum:Array, thinkingBlocksAccum?:Array, resetQueryTimer:Function, pauseQueryTimer?:Function, touchQueryTimer?:Function, stopQueryTimer?:Function, markEngineTerminal?:Function, sessionId?:string, vpId?:string, turnId?:string}} hctx
4091
4126
  */
4092
4127
  export function __testHandleEngineEvent(event, hctx) {
4093
4128
  return handleEngineEvent(event, hctx);
4094
4129
  }
4095
4130
 
4096
4131
  function handleEngineEvent(event, hctx) {
4132
+ // Hidden progress has no wire/history/status projection and must not flush
4133
+ // pending visible text or resume a paused/terminal provider watchdog.
4134
+ if (event.type === 'provider_activity') {
4135
+ hctx.touchQueryTimer?.();
4136
+ return;
4137
+ }
4138
+ if ((event.type === 'text_delta' || event.type === 'thinking_delta')
4139
+ && (typeof event.text !== 'string' || event.text.length === 0)) return;
4097
4140
  const terminalTurnEnd = event.type === 'turn_end' && event.terminal === true;
4098
4141
  const managesQueryTimer = terminalTurnEnd
4142
+ || event.type === 'aborted'
4143
+ || event.type === 'usage'
4099
4144
  || event.type === 'tool_start'
4100
4145
  || event.type === 'tool_end'
4101
4146
  || event.type === 'async_task_wait_start'
@@ -4300,6 +4345,7 @@ function handleEngineEvent(event, hctx) {
4300
4345
  break;
4301
4346
 
4302
4347
  case 'aborted':
4348
+ (hctx.stopQueryTimer || hctx.pauseQueryTimer)?.();
4303
4349
  if (typeof hctx.markEngineTerminal === 'function') {
4304
4350
  hctx.markEngineTerminal('aborted', { reason: event.reason || 'external' });
4305
4351
  }
@@ -4309,8 +4355,8 @@ function handleEngineEvent(event, hctx) {
4309
4355
  // Most engine turn_end events are internal loop boundaries. Only the
4310
4356
  // explicit terminal event precedes post-turn persistence/maintenance;
4311
4357
  // stop the user-query silence watchdog before that best-effort work.
4312
- if (event.terminal && typeof hctx.pauseQueryTimer === 'function') {
4313
- hctx.pauseQueryTimer();
4358
+ if (event.terminal) {
4359
+ (hctx.stopQueryTimer || hctx.pauseQueryTimer)?.();
4314
4360
  }
4315
4361
  // A normal tool_use stop means "run tools, then call the adapter again",
4316
4362
  // so it must NOT end the VP's visible turn. An aborted turn is terminal
@@ -5282,7 +5328,7 @@ function startSessionLoadInBackground({ sessionId = null, sessionMeta = null, pe
5282
5328
  /**
5283
5329
  * Wrap {@link runVpTurn} with a second-stage abort escalation.
5284
5330
  *
5285
- * The in-turn watchdog is activity based: every engine event resets its
5331
+ * The in-turn watchdog is activity based: content progress resets its
5286
5332
  * silence timer, and only a genuinely silent turn calls `vpAbort.abort()`.
5287
5333
  * This wrapper must therefore start its grace period from that abort signal,
5288
5334
  * not from turn enqueue. A fixed enqueue deadline incorrectly terminates
@@ -5507,21 +5553,16 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5507
5553
  };
5508
5554
 
5509
5555
  try {
5510
- let queryTimer = null;
5511
5556
  const queryTimeoutMs = queryTimeoutMsForSession(sessionId);
5512
- const pauseQueryTimer = () => {
5513
- if (queryTimer) clearTimeout(queryTimer);
5514
- queryTimer = null;
5515
- };
5516
- const resetQueryTimer = () => {
5517
- pauseQueryTimer();
5518
- queryTimer = setTimeout(() => {
5519
- if (!vpAbort.signal.aborted) {
5520
- console.error(`[Yeaft] query timeout after ${queryTimeoutMs / 1000}s of silence — aborting VP ${vpId}`);
5521
- try { vpAbort.abort(); } catch { /* best-effort */ }
5522
- }
5523
- }, queryTimeoutMs);
5524
- };
5557
+ const watchdog = createQueryWatchdog({
5558
+ signal: vpAbort.signal,
5559
+ timeoutMs: queryTimeoutMs,
5560
+ onTimeout: () => {
5561
+ console.error(`[Yeaft] query timeout after ${queryTimeoutMs / 1000}s of silence — aborting VP ${vpId}`);
5562
+ try { vpAbort.abort(); } catch { /* best-effort */ }
5563
+ },
5564
+ });
5565
+ const { reset: resetQueryTimer, pause: pauseQueryTimer } = watchdog;
5525
5566
  resetQueryTimer();
5526
5567
 
5527
5568
  // Emit turn_start so frontend can create the message block.
@@ -5605,6 +5646,8 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5605
5646
  thinkingBlocksAccum,
5606
5647
  resetQueryTimer,
5607
5648
  pauseQueryTimer,
5649
+ touchQueryTimer: watchdog.touch,
5650
+ stopQueryTimer: watchdog.stop,
5608
5651
  sessionId,
5609
5652
  vpId,
5610
5653
  turnId,
@@ -5656,10 +5699,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5656
5699
  // Human think time is not engine silence. Pause the query watchdog,
5657
5700
  // but keep a separate bounded AskUser lifetime so an abandoned card
5658
5701
  // cannot pin the VP forever.
5659
- if (queryTimer) {
5660
- clearTimeout(queryTimer);
5661
- queryTimer = null;
5662
- }
5702
+ pauseQueryTimer();
5663
5703
  const pending = {
5664
5704
  resolve,
5665
5705
  reject,
@@ -5706,7 +5746,6 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5706
5746
  messageType: event?.type || null,
5707
5747
  });
5708
5748
  }
5709
- resetQueryTimer();
5710
5749
  handleEngineEvent(event, handlerCtx);
5711
5750
  }
5712
5751
  if (perfTraceId) {
@@ -5781,7 +5820,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5781
5820
  emitVpTurnEnd('end_turn');
5782
5821
  }
5783
5822
  } finally {
5784
- if (queryTimer) clearTimeout(queryTimer);
5823
+ watchdog.stop();
5785
5824
  }
5786
5825
  } catch (err) {
5787
5826
  if (escalationState?.escalated) return;
@@ -8068,6 +8107,7 @@ export const __testHooks = {
8068
8107
  projectRuntimeCount() {
8069
8108
  return projectRuntimes.size;
8070
8109
  },
8110
+ createQueryWatchdog,
8071
8111
  queryTimeoutMsForSessionConfig,
8072
8112
  queryTimeoutMsForSession,
8073
8113
  seedQueuedVpTurn({ sessionId = 'session-test', vpId = 'vp-test', threadId = 'main', turnId = 'turn-test' } = {}) {
@@ -1394,6 +1394,9 @@ export class WorkItemRunner {
1394
1394
  stoppedByEngineEvent = true;
1395
1395
  break;
1396
1396
  }
1397
+ // Provider activity is liveness-only; keep control handling above, but
1398
+ // never turn it into durable Run progress or a UI broadcast.
1399
+ if (event?.type === 'provider_activity') continue;
1397
1400
  if (event?.type === 'loop') {
1398
1401
  loopCount += 1;
1399
1402
  this.store.appendRunLoop?.(run.id, ownerBootId, run.leaseEpoch, {