@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.
- package/local-runtime/version.json +1 -1
- package/package.json +1 -1
- package/yeaft/engine.js +6 -0
- package/yeaft/llm/adapter.js +2 -1
- package/yeaft/llm/anthropic.js +13 -3
- package/yeaft/llm/openai-responses.js +9 -2
- package/yeaft/models.js +3 -2
- package/yeaft/sub-agent/runner.js +2 -0
- package/yeaft/web-bridge.js +65 -25
- package/yeaft/work-center/runner.js +3 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.578"}
|
package/package.json
CHANGED
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':
|
package/yeaft/llm/adapter.js
CHANGED
|
@@ -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 ─────────────────────────────────────
|
package/yeaft/llm/anthropic.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
|
500
|
-
|
|
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
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -1157,7 +1157,7 @@ export function broadcastLanguageChange(language) {
|
|
|
1157
1157
|
applyLiveLanguage(language);
|
|
1158
1158
|
}
|
|
1159
1159
|
|
|
1160
|
-
/**
|
|
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
|
|
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:
|
|
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
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|