@yeaft/webchat-agent 0.1.1104 → 0.1.1107
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/cli.js +28 -17
- package/connection/index.js +6 -3
- package/connection/message-router.js +10 -6
- package/index.js +4 -2
- package/llm-config-cli.js +48 -0
- package/package.json +1 -1
- package/service/config.js +90 -15
- package/service/index.js +78 -40
- package/service/linux.js +48 -37
- package/service/macos.js +35 -30
- package/service/windows.js +46 -39
- package/service.js +10 -0
- package/terminal.js +14 -2
- package/yeaft/cli.js +7 -7
- package/yeaft/conversation/persist.js +28 -118
- package/yeaft/debug-trace.js +967 -695
- package/yeaft/engine.js +127 -96
- package/yeaft/llm/adapter.js +7 -3
- package/yeaft/llm/anthropic.js +56 -15
- package/yeaft/llm/openai-responses.js +22 -12
- package/yeaft/llm/router.js +16 -2
- package/yeaft/models.js +15 -2
- package/yeaft/session.js +9 -17
- package/yeaft/sessions/session-crud.js +42 -23
- package/yeaft/systemd-scope.js +88 -0
- package/yeaft/tasks/manager.js +81 -13
- package/yeaft/tasks/shell-runner.js +11 -3
- package/yeaft/tools/bash.js +9 -2
- package/yeaft/vp-status-broker.js +8 -3
- package/yeaft/web-bridge.js +221 -24
package/yeaft/engine.js
CHANGED
|
@@ -2294,18 +2294,139 @@ export class Engine {
|
|
|
2294
2294
|
stopReason = event.stopReason;
|
|
2295
2295
|
yield event;
|
|
2296
2296
|
break;
|
|
2297
|
-
case 'error':
|
|
2298
|
-
|
|
2299
|
-
|
|
2297
|
+
case 'error': {
|
|
2298
|
+
const adapterError = event.error instanceof Error
|
|
2299
|
+
? event.error
|
|
2300
|
+
: new Error(String(event.error?.message || event.error || 'LLM stream error'));
|
|
2301
|
+
adapterError.retryable = Boolean(event.retryable);
|
|
2302
|
+
if (event.retryable) {
|
|
2303
|
+
if (adapterError instanceof LLMRateLimitError || adapterError instanceof LLMServerError) {
|
|
2304
|
+
throw adapterError;
|
|
2305
|
+
}
|
|
2306
|
+
throw new LLMServerError(adapterError.message, adapterError.statusCode ?? 0);
|
|
2307
|
+
}
|
|
2308
|
+
if (adapterError instanceof LLMRateLimitError || adapterError instanceof LLMServerError) {
|
|
2309
|
+
const nonRetryableError = new Error(adapterError.message);
|
|
2310
|
+
nonRetryableError.name = adapterError.name || 'LLMStreamError';
|
|
2311
|
+
nonRetryableError.code = adapterError.code;
|
|
2312
|
+
nonRetryableError.statusCode = adapterError.statusCode;
|
|
2313
|
+
nonRetryableError.retryable = false;
|
|
2314
|
+
throw nonRetryableError;
|
|
2315
|
+
}
|
|
2316
|
+
throw adapterError;
|
|
2317
|
+
}
|
|
2300
2318
|
}
|
|
2301
2319
|
}
|
|
2302
2320
|
// Stream completed without throwing — reset the retry counter so
|
|
2303
|
-
// the next turn starts with a clean budget.
|
|
2304
|
-
//
|
|
2305
|
-
// throw), since those are policy-specific and not transport-level.
|
|
2321
|
+
// the next turn starts with a clean budget. In-band adapter errors
|
|
2322
|
+
// are converted to throws above so they share the real error path.
|
|
2306
2323
|
consecutiveRetryableErrors = 0;
|
|
2307
2324
|
} catch (err) {
|
|
2308
2325
|
const latencyMs = Date.now() - startTime;
|
|
2326
|
+
|
|
2327
|
+
const endAttemptTrace = (attemptStopReason) => {
|
|
2328
|
+
this.#trace.endTurn(turnId, {
|
|
2329
|
+
model: currentModel,
|
|
2330
|
+
inputTokens: totalUsage.inputTokens,
|
|
2331
|
+
outputTokens: totalUsage.outputTokens,
|
|
2332
|
+
cacheReadTokens: totalUsage.cacheReadTokens,
|
|
2333
|
+
cacheWriteTokens: totalUsage.cacheWriteTokens,
|
|
2334
|
+
stopReason: attemptStopReason,
|
|
2335
|
+
latencyMs,
|
|
2336
|
+
responseText,
|
|
2337
|
+
systemPrompt,
|
|
2338
|
+
messages: conversationMessages.map(mapDebugMessage),
|
|
2339
|
+
toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
|
|
2340
|
+
usage: {
|
|
2341
|
+
inputTokens: totalUsage.inputTokens || 0,
|
|
2342
|
+
outputTokens: totalUsage.outputTokens || 0,
|
|
2343
|
+
cacheReadTokens: totalUsage.cacheReadTokens || 0,
|
|
2344
|
+
cacheWriteTokens: totalUsage.cacheWriteTokens || 0,
|
|
2345
|
+
totalInputTokens: (totalUsage.inputTokens || 0) + (totalUsage.cacheInputDeltaTokens || 0),
|
|
2346
|
+
totalTokens: (totalUsage.inputTokens || 0) + (totalUsage.cacheInputDeltaTokens || 0) + (totalUsage.outputTokens || 0),
|
|
2347
|
+
},
|
|
2348
|
+
ttfbMs,
|
|
2349
|
+
rawRequest,
|
|
2350
|
+
rawResponse,
|
|
2351
|
+
});
|
|
2352
|
+
};
|
|
2353
|
+
|
|
2354
|
+
// Abort/retry/fallback are not final assistant responses. Handle them
|
|
2355
|
+
// before writing debug loop rows; otherwise transient DeepSeek stream
|
|
2356
|
+
// cuts or user stops show up as bogus `Error: Request aborted` replies.
|
|
2357
|
+
const earlyIsAbort = err instanceof LLMAbortError
|
|
2358
|
+
|| err?.name === 'AbortError'
|
|
2359
|
+
|| err?.name === 'LLMAbortError'
|
|
2360
|
+
|| (signal?.aborted && /abort/i.test(err?.message || ''));
|
|
2361
|
+
if (earlyIsAbort || signal?.aborted) {
|
|
2362
|
+
endAttemptTrace('aborted');
|
|
2363
|
+
yield { type: 'aborted', reason: this.#abortReason || 'external', turnNumber, threadId };
|
|
2364
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'aborted', threadId };
|
|
2365
|
+
break;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
if (err instanceof LLMContextError && this.#conversationStore) {
|
|
2369
|
+
const retryConsolidated = await this.#maybeConsolidate();
|
|
2370
|
+
if (retryConsolidated && retryConsolidated.archivedCount > 0) {
|
|
2371
|
+
endAttemptTrace('context_overflow_retry');
|
|
2372
|
+
yield { type: 'consolidate', archivedCount: retryConsolidated.archivedCount, extractedCount: retryConsolidated.extractedCount };
|
|
2373
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'context_overflow_retry', threadId };
|
|
2374
|
+
continue;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
const earlyIsRateLimit = err instanceof LLMRateLimitError;
|
|
2379
|
+
const earlyIsTransient = err instanceof LLMServerError;
|
|
2380
|
+
if (earlyIsRateLimit || earlyIsTransient) {
|
|
2381
|
+
if (consecutiveRetryableErrors < retryPolicy.maxRetries) {
|
|
2382
|
+
consecutiveRetryableErrors += 1;
|
|
2383
|
+
let delayMs;
|
|
2384
|
+
let reason;
|
|
2385
|
+
if (earlyIsRateLimit && Number.isFinite(err.retryAfterMs) && err.retryAfterMs > 0) {
|
|
2386
|
+
delayMs = Math.min(retryPolicy.maxDelayMs, err.retryAfterMs);
|
|
2387
|
+
reason = 'rate_limit_retry_after';
|
|
2388
|
+
} else if (earlyIsRateLimit) {
|
|
2389
|
+
delayMs = computeBackoffDelay(retryPolicy, consecutiveRetryableErrors);
|
|
2390
|
+
reason = 'rate_limit_backoff';
|
|
2391
|
+
} else {
|
|
2392
|
+
delayMs = computeBackoffDelay(retryPolicy, consecutiveRetryableErrors - 1);
|
|
2393
|
+
reason = err instanceof LLMStreamIdleTimeoutError
|
|
2394
|
+
? 'stream_idle_timeout'
|
|
2395
|
+
: 'transient_backoff';
|
|
2396
|
+
}
|
|
2397
|
+
endAttemptTrace('llm_retry');
|
|
2398
|
+
yield {
|
|
2399
|
+
type: 'llm_retry',
|
|
2400
|
+
attempt: consecutiveRetryableErrors,
|
|
2401
|
+
maxRetries: retryPolicy.maxRetries,
|
|
2402
|
+
delayMs,
|
|
2403
|
+
reason,
|
|
2404
|
+
errorName: err.name,
|
|
2405
|
+
statusCode: err.statusCode ?? null,
|
|
2406
|
+
message: String(err.message || '').slice(0, 300),
|
|
2407
|
+
};
|
|
2408
|
+
const slept = await sleepWithAbort(delayMs, signal);
|
|
2409
|
+
if (!slept || signal?.aborted) {
|
|
2410
|
+
yield { type: 'aborted', reason: this.#abortReason || 'external', turnNumber, threadId };
|
|
2411
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'aborted', threadId };
|
|
2412
|
+
break;
|
|
2413
|
+
}
|
|
2414
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'llm_retry', threadId };
|
|
2415
|
+
continue;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
const earlyFallbackModel = this.#config.fallbackModel;
|
|
2420
|
+
if (earlyFallbackModel && earlyFallbackModel !== currentModel
|
|
2421
|
+
&& (earlyIsRateLimit || earlyIsTransient)) {
|
|
2422
|
+
endAttemptTrace('fallback_retry');
|
|
2423
|
+
yield { type: 'fallback', from: currentModel, to: earlyFallbackModel, reason: err.message };
|
|
2424
|
+
currentModel = earlyFallbackModel;
|
|
2425
|
+
consecutiveRetryableErrors = 0;
|
|
2426
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'fallback_retry', threadId };
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2309
2430
|
this.#trace.endTurn(turnId, {
|
|
2310
2431
|
model: currentModel,
|
|
2311
2432
|
inputTokens: totalUsage.inputTokens,
|
|
@@ -2367,96 +2488,6 @@ export class Engine {
|
|
|
2367
2488
|
rawResponse,
|
|
2368
2489
|
};
|
|
2369
2490
|
|
|
2370
|
-
// ─── task-325a: abort short-circuit ────────────────
|
|
2371
|
-
// If the adapter threw LLMAbortError, or the signal fired during
|
|
2372
|
-
// stream() (fetch throws AbortError / DOMException), we converge
|
|
2373
|
-
// the state machine on the 'aborted' terminal state — no retry,
|
|
2374
|
-
// no fallback, no persistence. One `aborted` event + one
|
|
2375
|
-
// `turn_end` with stopReason='aborted' and we're done.
|
|
2376
|
-
const isAbort = err instanceof LLMAbortError
|
|
2377
|
-
|| err?.name === 'AbortError'
|
|
2378
|
-
|| err?.name === 'LLMAbortError'
|
|
2379
|
-
|| (signal?.aborted && /abort/i.test(err?.message || ''));
|
|
2380
|
-
if (isAbort || signal?.aborted) {
|
|
2381
|
-
yield { type: 'aborted', reason: this.#abortReason || 'external', turnNumber, threadId };
|
|
2382
|
-
yield { type: 'turn_end', turnNumber, stopReason: 'aborted', threadId };
|
|
2383
|
-
break;
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
// ─── LLMContextError → force compact → retry ──────
|
|
2387
|
-
if (err instanceof LLMContextError && this.#conversationStore) {
|
|
2388
|
-
const consolidated = await this.#maybeConsolidate();
|
|
2389
|
-
if (consolidated && consolidated.archivedCount > 0) {
|
|
2390
|
-
yield { type: 'consolidate', archivedCount: consolidated.archivedCount, extractedCount: consolidated.extractedCount };
|
|
2391
|
-
yield { type: 'turn_end', turnNumber, stopReason: 'context_overflow_retry', threadId };
|
|
2392
|
-
continue; // retry with fewer messages
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
// ─── Rate-limit / transient / stream-idle retry ───
|
|
2397
|
-
// Honour server-supplied Retry-After for 429/529; fall back to
|
|
2398
|
-
// exponential backoff for 5xx, transport failures, and stream-idle
|
|
2399
|
-
// timeouts wrapped as LLMServerError. Counts against
|
|
2400
|
-
// retryPolicy.maxRetries; on exhaustion we fall through to the
|
|
2401
|
-
// fallback-model path (and ultimately the error event) without
|
|
2402
|
-
// further waiting.
|
|
2403
|
-
const isRateLimit = err instanceof LLMRateLimitError;
|
|
2404
|
-
const isTransient = err instanceof LLMServerError;
|
|
2405
|
-
if (isRateLimit || isTransient) {
|
|
2406
|
-
if (consecutiveRetryableErrors < retryPolicy.maxRetries) {
|
|
2407
|
-
consecutiveRetryableErrors += 1;
|
|
2408
|
-
let delayMs;
|
|
2409
|
-
let reason;
|
|
2410
|
-
if (isRateLimit && Number.isFinite(err.retryAfterMs) && err.retryAfterMs > 0) {
|
|
2411
|
-
// Server told us exactly when to come back. Respect it,
|
|
2412
|
-
// but still cap to maxDelayMs so a misconfigured upstream
|
|
2413
|
-
// can't make us hang forever on one turn.
|
|
2414
|
-
delayMs = Math.min(retryPolicy.maxDelayMs, err.retryAfterMs);
|
|
2415
|
-
reason = 'rate_limit_retry_after';
|
|
2416
|
-
} else if (isRateLimit) {
|
|
2417
|
-
// No header — use backoff but start one step higher so the
|
|
2418
|
-
// first retry isn't immediate (rate-limit windows are
|
|
2419
|
-
// usually >= 1s wide).
|
|
2420
|
-
delayMs = computeBackoffDelay(retryPolicy, consecutiveRetryableErrors);
|
|
2421
|
-
reason = 'rate_limit_backoff';
|
|
2422
|
-
} else {
|
|
2423
|
-
delayMs = computeBackoffDelay(retryPolicy, consecutiveRetryableErrors - 1);
|
|
2424
|
-
reason = err instanceof LLMStreamIdleTimeoutError
|
|
2425
|
-
? 'stream_idle_timeout'
|
|
2426
|
-
: 'transient_backoff';
|
|
2427
|
-
}
|
|
2428
|
-
yield {
|
|
2429
|
-
type: 'llm_retry',
|
|
2430
|
-
attempt: consecutiveRetryableErrors,
|
|
2431
|
-
maxRetries: retryPolicy.maxRetries,
|
|
2432
|
-
delayMs,
|
|
2433
|
-
reason,
|
|
2434
|
-
errorName: err.name,
|
|
2435
|
-
statusCode: err.statusCode ?? null,
|
|
2436
|
-
message: String(err.message || '').slice(0, 300),
|
|
2437
|
-
};
|
|
2438
|
-
const slept = await sleepWithAbort(delayMs, signal);
|
|
2439
|
-
if (!slept || signal?.aborted) {
|
|
2440
|
-
yield { type: 'aborted', reason: this.#abortReason || 'external', turnNumber, threadId };
|
|
2441
|
-
yield { type: 'turn_end', turnNumber, stopReason: 'aborted', threadId };
|
|
2442
|
-
break;
|
|
2443
|
-
}
|
|
2444
|
-
yield { type: 'turn_end', turnNumber, stopReason: 'llm_retry', threadId };
|
|
2445
|
-
continue; // retry the same turn with the same model
|
|
2446
|
-
}
|
|
2447
|
-
// Exhausted — fall through to fallback-model / error paths.
|
|
2448
|
-
}
|
|
2449
|
-
|
|
2450
|
-
// ─── Fallback model ──────────────────────────────
|
|
2451
|
-
const fallbackModel = this.#config.fallbackModel;
|
|
2452
|
-
if (fallbackModel && fallbackModel !== currentModel &&
|
|
2453
|
-
(err instanceof LLMRateLimitError || err instanceof LLMServerError)) {
|
|
2454
|
-
yield { type: 'fallback', from: currentModel, to: fallbackModel, reason: err.message };
|
|
2455
|
-
currentModel = fallbackModel;
|
|
2456
|
-
consecutiveRetryableErrors = 0; // new model, fresh retry budget
|
|
2457
|
-
yield { type: 'turn_end', turnNumber, stopReason: 'fallback_retry', threadId };
|
|
2458
|
-
continue; // retry with fallback model
|
|
2459
|
-
}
|
|
2460
2491
|
|
|
2461
2492
|
const isRetryableError = err instanceof LLMRateLimitError || err instanceof LLMServerError;
|
|
2462
2493
|
const errorEvent = {
|
package/yeaft/llm/adapter.js
CHANGED
|
@@ -240,12 +240,17 @@ export function retryAfterFromResponse(response) {
|
|
|
240
240
|
* prove from the error shape.
|
|
241
241
|
*
|
|
242
242
|
* @param {unknown} err
|
|
243
|
-
* @param {{ providerLabel?: string }} [opts]
|
|
243
|
+
* @param {{ providerLabel?: string, signal?: AbortSignal }} [opts]
|
|
244
244
|
* @returns {Error}
|
|
245
245
|
*/
|
|
246
246
|
export function classifyFetchError(err, opts = {}) {
|
|
247
247
|
if (!(err instanceof Error)) return err instanceof Object ? err : new Error(String(err));
|
|
248
|
-
if (err.name === '
|
|
248
|
+
if (err.name === 'LLMAbortError' || err instanceof LLMAbortError) return new LLMAbortError();
|
|
249
|
+
const label = opts.providerLabel ? `${opts.providerLabel}: ` : '';
|
|
250
|
+
if (err.name === 'AbortError') {
|
|
251
|
+
if (opts.signal?.aborted) return new LLMAbortError();
|
|
252
|
+
return new LLMServerError(`${label}stream aborted unexpectedly: ${err.message || 'AbortError'}`, 0);
|
|
253
|
+
}
|
|
249
254
|
// Anything already classified: keep as-is.
|
|
250
255
|
if (err instanceof LLMRateLimitError
|
|
251
256
|
|| err instanceof LLMAuthError
|
|
@@ -254,7 +259,6 @@ export function classifyFetchError(err, opts = {}) {
|
|
|
254
259
|
|| err instanceof LLMAbortError) {
|
|
255
260
|
return err;
|
|
256
261
|
}
|
|
257
|
-
const label = opts.providerLabel ? `${opts.providerLabel}: ` : '';
|
|
258
262
|
const code = err.cause?.code || err.code || null;
|
|
259
263
|
const transientCodes = new Set([
|
|
260
264
|
'ECONNRESET', 'ECONNREFUSED', 'ECONNABORTED', 'ETIMEDOUT',
|
package/yeaft/llm/anthropic.js
CHANGED
|
@@ -34,10 +34,10 @@ function thinkingV1Enabled() {
|
|
|
34
34
|
return process.env.YEAFT_THINKING_V1 === '1';
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function applyAnthropicThinking(body, model, effort) {
|
|
38
|
-
const cap = getThinkingCapability(model);
|
|
37
|
+
function applyAnthropicThinking(body, model, effort, effortContext = {}) {
|
|
38
|
+
const cap = getThinkingCapability(model, effortContext);
|
|
39
39
|
if (!cap.supportsThinking) return;
|
|
40
|
-
if (!getModelEffortOptions(model).includes(effort)) return;
|
|
40
|
+
if (!getModelEffortOptions(model, effortContext).includes(effort)) return;
|
|
41
41
|
|
|
42
42
|
if (cap.thinkingProtocol === 'anthropic-adaptive') {
|
|
43
43
|
body.thinking = { type: 'adaptive' };
|
|
@@ -59,6 +59,34 @@ function applyAnthropicThinking(body, model, effort) {
|
|
|
59
59
|
const DEFAULT_BASE_URL = 'https://api.anthropic.com';
|
|
60
60
|
const API_VERSION = '2023-06-01';
|
|
61
61
|
|
|
62
|
+
function hasNonEmptyText(value) {
|
|
63
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function translateUserContent(content) {
|
|
67
|
+
if (hasNonEmptyText(content)) return content;
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(content)) {
|
|
70
|
+
const parts = [];
|
|
71
|
+
for (const part of content) {
|
|
72
|
+
if (!part || typeof part !== 'object') {
|
|
73
|
+
if (hasNonEmptyText(part)) parts.push({ type: 'text', text: String(part) });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (part.type === 'text') {
|
|
77
|
+
if (hasNonEmptyText(part.text)) parts.push(part);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// Non-text blocks (image/document/tool_result-like compatible payloads)
|
|
81
|
+
// are meaningful even without a text field. Preserve them verbatim.
|
|
82
|
+
parts.push(part);
|
|
83
|
+
}
|
|
84
|
+
return parts.length > 0 ? parts : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
62
90
|
/**
|
|
63
91
|
* AnthropicAdapter — Talks to Anthropic Messages API.
|
|
64
92
|
*/
|
|
@@ -116,7 +144,8 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
116
144
|
for (const msg of messages) {
|
|
117
145
|
if (msg.role === 'system') continue; // system goes separately
|
|
118
146
|
if (msg.role === 'user') {
|
|
119
|
-
|
|
147
|
+
const content = translateUserContent(msg.content);
|
|
148
|
+
if (content) result.push({ role: 'user', content });
|
|
120
149
|
} else if (msg.role === 'assistant') {
|
|
121
150
|
const content = [];
|
|
122
151
|
// task-327d: Anthropic requires thinking blocks to appear BEFORE
|
|
@@ -137,7 +166,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
137
166
|
}
|
|
138
167
|
}
|
|
139
168
|
}
|
|
140
|
-
if (msg.content) {
|
|
169
|
+
if (hasNonEmptyText(msg.content)) {
|
|
141
170
|
content.push({ type: 'text', text: msg.content });
|
|
142
171
|
}
|
|
143
172
|
if (msg.toolCalls) {
|
|
@@ -150,7 +179,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
150
179
|
});
|
|
151
180
|
}
|
|
152
181
|
}
|
|
153
|
-
result.push({ role: 'assistant', content });
|
|
182
|
+
if (content.length > 0) result.push({ role: 'assistant', content });
|
|
154
183
|
} else if (msg.role === 'tool') {
|
|
155
184
|
// Anthropic requires all tool_results from the same turn in a single
|
|
156
185
|
// user message. Merge consecutive tool messages into one.
|
|
@@ -207,10 +236,10 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
207
236
|
}
|
|
208
237
|
|
|
209
238
|
/**
|
|
210
|
-
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'xhigh'|'max', effortSource?: 'user'|'auto', signal?: AbortSignal }} params
|
|
239
|
+
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'xhigh'|'max', effortSource?: 'user'|'auto', effortContext?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
|
|
211
240
|
* @returns {AsyncGenerator<import('./adapter.js').StreamEvent>}
|
|
212
241
|
*/
|
|
213
|
-
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, signal, onRawExchange }) {
|
|
242
|
+
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext, signal, onRawExchange }) {
|
|
214
243
|
if (signal?.aborted) throw new LLMAbortError();
|
|
215
244
|
|
|
216
245
|
const body = {
|
|
@@ -226,7 +255,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
226
255
|
// models use budget_tokens. Unsupported combinations silently drop effort.
|
|
227
256
|
const normEffort = normalizeEffort(effort);
|
|
228
257
|
if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
|
|
229
|
-
applyAnthropicThinking(body, model, normEffort);
|
|
258
|
+
applyAnthropicThinking(body, model, normEffort, effortContext);
|
|
230
259
|
}
|
|
231
260
|
|
|
232
261
|
const translatedTools = this.#translateTools(tools);
|
|
@@ -249,7 +278,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
249
278
|
signal,
|
|
250
279
|
});
|
|
251
280
|
} catch (err) {
|
|
252
|
-
throw classifyFetchError(err, { providerLabel: 'Anthropic' });
|
|
281
|
+
throw classifyFetchError(err, { providerLabel: 'Anthropic', signal });
|
|
253
282
|
}
|
|
254
283
|
|
|
255
284
|
if (!response.ok) {
|
|
@@ -291,6 +320,8 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
291
320
|
const rawSseBodyChunks = [];
|
|
292
321
|
const responseHeaders = safeHeaders(response);
|
|
293
322
|
const responseStatus = response.status;
|
|
323
|
+
let sawStop = false;
|
|
324
|
+
let sawMessageStart = false;
|
|
294
325
|
|
|
295
326
|
try {
|
|
296
327
|
while (true) {
|
|
@@ -310,7 +341,10 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
310
341
|
for (const line of lines) {
|
|
311
342
|
if (!line.startsWith('data: ')) continue;
|
|
312
343
|
const data = line.slice(6).trim();
|
|
313
|
-
if (data === '[DONE]')
|
|
344
|
+
if (data === '[DONE]') {
|
|
345
|
+
sawStop = true;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
314
348
|
|
|
315
349
|
let event;
|
|
316
350
|
try {
|
|
@@ -410,6 +444,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
410
444
|
} else if (type === 'message_delta') {
|
|
411
445
|
const stopReason = event.delta?.stop_reason;
|
|
412
446
|
if (stopReason) {
|
|
447
|
+
sawStop = true;
|
|
413
448
|
yield {
|
|
414
449
|
type: 'stop',
|
|
415
450
|
stopReason: this.#mapStopReason(stopReason),
|
|
@@ -423,7 +458,10 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
423
458
|
outputTokens: event.usage.output_tokens || 0,
|
|
424
459
|
};
|
|
425
460
|
}
|
|
461
|
+
} else if (type === 'message_stop') {
|
|
462
|
+
sawStop = true;
|
|
426
463
|
} else if (type === 'message_start') {
|
|
464
|
+
sawMessageStart = true;
|
|
427
465
|
// Usage from message_start
|
|
428
466
|
if (event.message?.usage) {
|
|
429
467
|
yield {
|
|
@@ -443,8 +481,11 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
443
481
|
}
|
|
444
482
|
}
|
|
445
483
|
}
|
|
484
|
+
if (sawMessageStart && !sawStop) {
|
|
485
|
+
throw new LLMServerError('Anthropic stream ended before stop event', 0);
|
|
486
|
+
}
|
|
446
487
|
} catch (err) {
|
|
447
|
-
throw classifyFetchError(err, { providerLabel: 'Anthropic' });
|
|
488
|
+
throw classifyFetchError(err, { providerLabel: 'Anthropic', signal });
|
|
448
489
|
} finally {
|
|
449
490
|
reader.releaseLock();
|
|
450
491
|
// Emit raw exchange after stream completes (or errors). Body is the
|
|
@@ -473,7 +514,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
473
514
|
* models silently drop the param. max_tokens auto-widens to budget+1024
|
|
474
515
|
* when needed.
|
|
475
516
|
*/
|
|
476
|
-
async call({ model, system, messages, maxTokens = 4096, effort, effortSource, signal }) {
|
|
517
|
+
async call({ model, system, messages, maxTokens = 4096, effort, effortSource, effortContext, signal }) {
|
|
477
518
|
if (signal?.aborted) throw new LLMAbortError();
|
|
478
519
|
|
|
479
520
|
const body = {
|
|
@@ -486,7 +527,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
486
527
|
// task-327c: mirror stream()'s thinking injection for side queries.
|
|
487
528
|
const normEffort = normalizeEffort(effort);
|
|
488
529
|
if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
|
|
489
|
-
applyAnthropicThinking(body, model, normEffort);
|
|
530
|
+
applyAnthropicThinking(body, model, normEffort, effortContext);
|
|
490
531
|
}
|
|
491
532
|
|
|
492
533
|
let response;
|
|
@@ -498,7 +539,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
498
539
|
signal,
|
|
499
540
|
});
|
|
500
541
|
} catch (err) {
|
|
501
|
-
throw classifyFetchError(err, { providerLabel: 'Anthropic' });
|
|
542
|
+
throw classifyFetchError(err, { providerLabel: 'Anthropic', signal });
|
|
502
543
|
}
|
|
503
544
|
|
|
504
545
|
if (!response.ok) {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - response.function_call_arguments.delta / .done
|
|
18
18
|
* - response.completed (contains final response.usage)
|
|
19
19
|
* - response.incomplete (e.g. max_output_tokens)
|
|
20
|
-
* - response.
|
|
20
|
+
* - response.failed
|
|
21
21
|
* - Usage: only in terminal completed/incomplete events
|
|
22
22
|
*
|
|
23
23
|
* Id contract (agreed with PM):
|
|
@@ -292,8 +292,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
292
292
|
signal,
|
|
293
293
|
});
|
|
294
294
|
} catch (err) {
|
|
295
|
-
|
|
296
|
-
throw classifyFetchError(err, { providerLabel: 'OpenAI' });
|
|
295
|
+
throw classifyFetchError(err, { providerLabel: 'OpenAI', signal });
|
|
297
296
|
}
|
|
298
297
|
|
|
299
298
|
if (!response.ok) {
|
|
@@ -332,6 +331,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
332
331
|
const rawSseBodyChunks = [];
|
|
333
332
|
const responseHeaders = safeHeaders(response);
|
|
334
333
|
const responseStatus = response.status;
|
|
334
|
+
let sawTerminalEvent = false;
|
|
335
335
|
|
|
336
336
|
try {
|
|
337
337
|
while (true) {
|
|
@@ -353,7 +353,8 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
353
353
|
const line = rawLine.trimEnd();
|
|
354
354
|
if (!line.startsWith('data:')) continue;
|
|
355
355
|
const data = line.slice(5).trim();
|
|
356
|
-
if (!data
|
|
356
|
+
if (!data) continue;
|
|
357
|
+
if (data === '[DONE]') continue;
|
|
357
358
|
|
|
358
359
|
let event;
|
|
359
360
|
try {
|
|
@@ -409,6 +410,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
409
410
|
toolCallAccum.delete(idx);
|
|
410
411
|
}
|
|
411
412
|
} else if (type === 'response.completed' || type === 'response.incomplete') {
|
|
413
|
+
sawTerminalEvent = true;
|
|
412
414
|
const respObj = event.response || {};
|
|
413
415
|
|
|
414
416
|
// Fallback: flush any function_call items in the final output that we
|
|
@@ -449,17 +451,26 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
449
451
|
type: 'stop',
|
|
450
452
|
stopReason: this.#mapStopReason(respObj, sawToolCall),
|
|
451
453
|
};
|
|
452
|
-
} else if (type === 'response.error') {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
454
|
+
} else if (type === 'response.failed' || type === 'response.error') {
|
|
455
|
+
sawTerminalEvent = true;
|
|
456
|
+
// Let the engine decide; emit error event. `response.failed` is
|
|
457
|
+
// the official terminal event; keep `response.error` as a legacy
|
|
458
|
+
// compatibility alias for older mocks/proxies.
|
|
459
|
+
const errObj = event.response?.error || event.error || {};
|
|
460
|
+
const code = errObj.code || event.response?.status || type;
|
|
461
|
+
const message = errObj.message || event.message || `${type}: ${code}`;
|
|
462
|
+
const failure = new Error(message);
|
|
463
|
+
failure.code = code;
|
|
464
|
+
yield { type: 'error', error: failure, retryable: false };
|
|
456
465
|
}
|
|
457
466
|
// Other semantic events (output_item.done, content_part.added, etc.) are ignored.
|
|
458
467
|
}
|
|
459
468
|
}
|
|
469
|
+
if (!sawTerminalEvent) {
|
|
470
|
+
throw new LLMServerError('OpenAI stream ended before terminal event', 0);
|
|
471
|
+
}
|
|
460
472
|
} catch (err) {
|
|
461
|
-
|
|
462
|
-
throw classifyFetchError(err, { providerLabel: 'OpenAI' });
|
|
473
|
+
throw classifyFetchError(err, { providerLabel: 'OpenAI', signal });
|
|
463
474
|
} finally {
|
|
464
475
|
try { reader.releaseLock(); } catch { /* noop */ }
|
|
465
476
|
// Emit raw exchange after stream completes (or errors). Body is the
|
|
@@ -523,8 +534,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
523
534
|
signal,
|
|
524
535
|
});
|
|
525
536
|
} catch (err) {
|
|
526
|
-
|
|
527
|
-
throw classifyFetchError(err, { providerLabel: 'OpenAI' });
|
|
537
|
+
throw classifyFetchError(err, { providerLabel: 'OpenAI', signal });
|
|
528
538
|
}
|
|
529
539
|
|
|
530
540
|
if (!response.ok) {
|
package/yeaft/llm/router.js
CHANGED
|
@@ -552,9 +552,16 @@ export class AdapterRouter extends LLMAdapter {
|
|
|
552
552
|
*/
|
|
553
553
|
async *stream(params) {
|
|
554
554
|
const resolved = await this.#resolveAdapter(params.model);
|
|
555
|
+
const effortContext = {
|
|
556
|
+
protocol: resolved.protocol,
|
|
557
|
+
supportsEffort: resolved.entry?.supportsEffort,
|
|
558
|
+
effortOptions: resolved.entry?.effortOptions,
|
|
559
|
+
thinkingProtocol: resolved.entry?.thinkingProtocol,
|
|
560
|
+
maxBudgetTokens: resolved.entry?.maxBudgetTokens,
|
|
561
|
+
};
|
|
555
562
|
const filtered = filterEffortForModel({ ...params, model: resolved.modelId }, resolved);
|
|
556
563
|
const sanitized = sanitizeMessagesForWire(filtered);
|
|
557
|
-
yield* resolved.adapter.stream({ ...sanitized, model: resolved.modelId });
|
|
564
|
+
yield* resolved.adapter.stream({ ...sanitized, model: resolved.modelId, effortContext });
|
|
558
565
|
}
|
|
559
566
|
|
|
560
567
|
/**
|
|
@@ -565,9 +572,16 @@ export class AdapterRouter extends LLMAdapter {
|
|
|
565
572
|
*/
|
|
566
573
|
async call(params) {
|
|
567
574
|
const resolved = await this.#resolveAdapter(params.model);
|
|
575
|
+
const effortContext = {
|
|
576
|
+
protocol: resolved.protocol,
|
|
577
|
+
supportsEffort: resolved.entry?.supportsEffort,
|
|
578
|
+
effortOptions: resolved.entry?.effortOptions,
|
|
579
|
+
thinkingProtocol: resolved.entry?.thinkingProtocol,
|
|
580
|
+
maxBudgetTokens: resolved.entry?.maxBudgetTokens,
|
|
581
|
+
};
|
|
568
582
|
const filtered = filterEffortForModel({ ...params, model: resolved.modelId }, resolved);
|
|
569
583
|
const sanitized = sanitizeMessagesForWire(filtered);
|
|
570
|
-
return resolved.adapter.call({ ...sanitized, model: resolved.modelId });
|
|
584
|
+
return resolved.adapter.call({ ...sanitized, model: resolved.modelId, effortContext });
|
|
571
585
|
}
|
|
572
586
|
|
|
573
587
|
/**
|
package/yeaft/models.js
CHANGED
|
@@ -537,8 +537,13 @@ export function thinkingBudgetForEffort(model, effort) {
|
|
|
537
537
|
*/
|
|
538
538
|
export function getThinkingCapability(model, context = {}) {
|
|
539
539
|
const info = MODEL_REGISTRY.get(model);
|
|
540
|
+
const modelId = parseModelRef(model).modelId;
|
|
540
541
|
const overrideOptions = normalizeEffortOptions(context.effortOptions);
|
|
541
|
-
const overrideProtocol = context.thinkingProtocol || (
|
|
542
|
+
const overrideProtocol = context.thinkingProtocol || (
|
|
543
|
+
context.protocol === 'anthropic'
|
|
544
|
+
? (/^deepseek/i.test(modelId) ? 'anthropic-adaptive' : 'anthropic')
|
|
545
|
+
: context.protocol === 'openai-responses' ? 'openai-reasoning' : null
|
|
546
|
+
);
|
|
542
547
|
if (context.supportsEffort === true || overrideOptions) {
|
|
543
548
|
return {
|
|
544
549
|
supportsThinking: true,
|
|
@@ -560,7 +565,15 @@ export function getThinkingCapability(model, context = {}) {
|
|
|
560
565
|
};
|
|
561
566
|
}
|
|
562
567
|
if (context.protocol === 'anthropic' && inferred?.thinkingProtocol === 'openai-reasoning') {
|
|
563
|
-
|
|
568
|
+
if (/^deepseek/i.test(modelId)) {
|
|
569
|
+
inferred = {
|
|
570
|
+
...inferred,
|
|
571
|
+
thinkingProtocol: 'anthropic-adaptive',
|
|
572
|
+
effortOptions: ANTHROPIC_ADAPTIVE_EFFORT_OPTIONS,
|
|
573
|
+
};
|
|
574
|
+
} else {
|
|
575
|
+
inferred = null;
|
|
576
|
+
}
|
|
564
577
|
}
|
|
565
578
|
if ((!info || !info.supportsThinking) && !inferred) {
|
|
566
579
|
return {
|
package/yeaft/session.js
CHANGED
|
@@ -218,26 +218,18 @@ export async function loadSession(options = {}) {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
// ─── 3. Create trajectory trace ─────────────────────────
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// (
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
// Cost is negligible (one insert per turn, WAL mode), and the data only
|
|
228
|
-
// accumulates while the user actually uses the agent. The previous gate
|
|
229
|
-
// silently discarded every turn unless the user had set debug:true in
|
|
230
|
-
// ~/.yeaft/config.json, which nobody ever did — wasting the asset.
|
|
221
|
+
// Always-on request trace for the Debug panel. It is file-backed, not
|
|
222
|
+
// SQLite-backed: each request writes one bounded JSON file under
|
|
223
|
+
// `<yeaftDir>/debug/` (session traces are nested under
|
|
224
|
+
// `<yeaftDir>/sessions/<sessionId>/debug/requests/`). A request file stores one
|
|
225
|
+
// base request snapshot plus per-loop deltas, so 100-200 loop requests do
|
|
226
|
+
// not become hundreds of tiny files or repeated cumulative payloads.
|
|
231
227
|
const trace = createTrace({
|
|
232
228
|
enabled: true,
|
|
233
|
-
|
|
229
|
+
dirPath: yeaftDir,
|
|
234
230
|
});
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
// not per turn. The always-on store stamps every turn with the *cumulative*
|
|
238
|
-
// request/response (each long-session row is ~MB), so without a tight TTL the
|
|
239
|
-
// file balloons — a real deployment hit 5GB in 15 days. 10 days keeps enough
|
|
240
|
-
// history for debug-panel replay while capping the steady-state footprint.
|
|
231
|
+
// Hard cap debug history to the most recent 10 requests per Session. Trace
|
|
232
|
+
// failures are best-effort and must never stop the agent loop.
|
|
241
233
|
try { trace.cleanup?.(10); } catch (err) {
|
|
242
234
|
console.warn('[Yeaft] trace.cleanup failed:', err?.message || err);
|
|
243
235
|
}
|