@yeaft/webchat-agent 1.0.504 → 1.0.506
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/conversation.js +4 -2
- package/local-runtime/server/handlers/agent-conversation.js +11 -9
- package/local-runtime/server/handlers/client-conversation.js +28 -2
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +99 -94
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +4 -3
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/local-runtime/web/vendor/katex/LICENSE +21 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Bold.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Italic.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Regular.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Math-Italic.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Script-Regular.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/local-runtime/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/local-runtime/web/vendor/katex/katex.min.css +1 -0
- package/local-runtime/web/vendor.bundle.js +1 -0
- package/local-runtime/web/vendor.bundle.js.gz +0 -0
- package/package.json +1 -1
- package/yeaft/cli.js +3 -0
- package/yeaft/config-api.js +16 -5
- package/yeaft/config.js +11 -2
- package/yeaft/conversation/history-index-worker.js +151 -0
- package/yeaft/conversation/history-index.js +96 -1
- package/yeaft/conversation/persist.js +21 -10
- package/yeaft/conversation/recall-relevance.js +103 -0
- package/yeaft/conversation/search.js +2 -1
- package/yeaft/debug-trace.js +15 -4
- package/yeaft/effort.js +159 -6
- package/yeaft/engine.js +180 -41
- package/yeaft/history-window.js +430 -5
- package/yeaft/llm/adapter.js +3 -2
- package/yeaft/llm/anthropic.js +97 -25
- package/yeaft/llm/openai-responses.js +71 -7
- package/yeaft/llm/provider-state.js +242 -0
- package/yeaft/llm/router.js +14 -2
- package/yeaft/llm/usage-accounting.js +3 -0
- package/yeaft/models.js +4 -1
- package/yeaft/snapshot-filter.js +1 -0
- package/yeaft/sub-agent/output-log.js +4 -0
- package/yeaft/sub-agent/prompt-queue.js +3 -0
- package/yeaft/sub-agent/runner.js +13 -2
- package/yeaft/tools/agent.js +3 -0
- package/yeaft/tools/send-message.js +2 -0
- package/yeaft/web-bridge.js +24 -17
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Deterministic, deliberately conservative relevance shared by indexed and
|
|
2
|
+
// in-memory turn recall. No provider calls and no transcript mutation.
|
|
3
|
+
const MAX_PROMPT_CHARS = 4096;
|
|
4
|
+
const MAX_TERMS = 8;
|
|
5
|
+
const STOP_WORDS = new Set(`a an and are as at be been but by can could did do does for from had has have how i if in is it its me my of on or our please should so that the their them there these they this to was we were what when where which who why will with would you your about again before earlier previous remember recall history message messages conversation turn turns tell show find help need want use using make get know explain answer question code file project problem fix work task test tests implementation implement change thanks continue revisit discuss discussed discussion decide decided follow-up
|
|
6
|
+
的 了 是 在 和 与 或 我 你 他 她 它 我们 你们 他们 这个 那个 什么 怎么 如何 为什么 请 请问 帮 帮我 帮忙 可以 能 不能 是否 需要 想 要 再 还 又 也 就 都 把 将 给 对 从 到 上 下 中 里 有 没有 一下 一些 一个 这些 那些 之前 以前 上次 刚才 历史 记得 回忆 召回 消息 对话 问题 回答 内容 事情 继续 现在 今天 昨天 后来 然后 相关 具体 代码 文件 项目 实现 修改 功能 测试 方案 方法 工作 任务 谢谢 好的 好 看看 查找 搜索 查询 处理 解决 进行 使用 讨论 提到 记忆 总结 回顾 提醒`.split(/\s+/u));
|
|
7
|
+
const segmenter = new Intl.Segmenter('zh', { granularity: 'word' });
|
|
8
|
+
|
|
9
|
+
function isIdentifier(term) {
|
|
10
|
+
return /[\p{L}\d][_.:/@-][\p{L}\d]/u.test(term)
|
|
11
|
+
|| /\p{L}.*\d|\d.*\p{L}/u.test(term)
|
|
12
|
+
|| /[a-z][A-Z]/u.test(term);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Extract at most eight meaningful lexical terms; generic prompts yield []. */
|
|
16
|
+
export function extractRecallTerms(prompt) {
|
|
17
|
+
// VP routing is not subject matter: "@vp-omni 继续" must not recall every
|
|
18
|
+
// earlier message addressed to that VP just because it looks like an ID.
|
|
19
|
+
const text = typeof prompt === 'string'
|
|
20
|
+
? prompt.slice(0, MAX_PROMPT_CHARS).replace(/@vp-[A-Za-z0-9_-]+\b/gu, ' ') : '';
|
|
21
|
+
const tokens = [];
|
|
22
|
+
// Keep paths, issue IDs, snake_case and camelCase intact before segmentation.
|
|
23
|
+
const rest = text.replace(/[\p{L}\p{N}]+(?:[_.:/@-][\p{L}\p{N}]+)+|[A-Za-z][A-Za-z0-9]*/gu, token => {
|
|
24
|
+
tokens.push(token);
|
|
25
|
+
return ' ';
|
|
26
|
+
});
|
|
27
|
+
let singleHan = '';
|
|
28
|
+
const flushHan = () => {
|
|
29
|
+
if (singleHan.length >= 2) tokens.push(singleHan);
|
|
30
|
+
singleHan = '';
|
|
31
|
+
};
|
|
32
|
+
for (const part of segmenter.segment(rest)) {
|
|
33
|
+
// ICU dictionaries sometimes split technical words such as 缓存 into
|
|
34
|
+
// individual Han characters. Preserve short adjacent runs, not stop words.
|
|
35
|
+
if (part.isWordLike && /^\p{Script=Han}$/u.test(part.segment) && !STOP_WORDS.has(part.segment)) {
|
|
36
|
+
singleHan += part.segment;
|
|
37
|
+
if (singleHan.length === 4) flushHan();
|
|
38
|
+
} else {
|
|
39
|
+
flushHan();
|
|
40
|
+
if (part.isWordLike) tokens.push(part.segment);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
flushHan();
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const terms = tokens.filter(term => {
|
|
46
|
+
const key = term.toLocaleLowerCase();
|
|
47
|
+
if (seen.has(key) || STOP_WORDS.has(key) || /^\d+$/u.test(key)
|
|
48
|
+
|| Array.from(key).length < 2 || key.length > 96) return false;
|
|
49
|
+
seen.add(key);
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
// Identifiers are more useful than prose if a long prompt exhausts the cap.
|
|
53
|
+
return terms.sort((a, b) => Number(isIdentifier(b)) - Number(isIdentifier(a))).slice(0, MAX_TERMS);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Score full visible turn text. stats.termDocumentFrequency may be a bounded
|
|
58
|
+
* candidate sample, not corpus-wide IDF; its sample size must be explicit.
|
|
59
|
+
* Returns explainable rejection reasons rather than weak positive matches.
|
|
60
|
+
*/
|
|
61
|
+
export function scoreRecallTurn(promptOrTerms, text, stats = {}) {
|
|
62
|
+
const terms = (Array.isArray(promptOrTerms) ? promptOrTerms : extractRecallTerms(promptOrTerms)).slice(0, MAX_TERMS);
|
|
63
|
+
const body = String(text || '').toLocaleLowerCase();
|
|
64
|
+
const matchedTerms = terms.filter(term => {
|
|
65
|
+
const key = term.toLocaleLowerCase();
|
|
66
|
+
if (/^[a-z0-9_]+$/u.test(key)) {
|
|
67
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
68
|
+
return new RegExp(`(?<![a-z0-9_])${escaped}(?![a-z0-9_])`, 'u').test(body);
|
|
69
|
+
}
|
|
70
|
+
return body.includes(key);
|
|
71
|
+
});
|
|
72
|
+
const identifierMatches = matchedTerms.filter(isIdentifier);
|
|
73
|
+
const independentTerms = matchedTerms.filter(term => !matchedTerms.some(other => (
|
|
74
|
+
other !== term && other.toLocaleLowerCase().includes(term.toLocaleLowerCase())
|
|
75
|
+
)));
|
|
76
|
+
const coverage = terms.length ? matchedTerms.length / terms.length : 0;
|
|
77
|
+
const sampleSize = Math.max(0, Number(stats.sampleSize) || 0);
|
|
78
|
+
const distinctiveness = matchedTerms.length ? Math.max(...matchedTerms.map(term => {
|
|
79
|
+
const frequency = stats.termDocumentFrequency?.[term.toLocaleLowerCase()];
|
|
80
|
+
return sampleSize >= 8 && Number.isFinite(frequency) ? 1 - frequency / sampleSize : 1;
|
|
81
|
+
})) : 0;
|
|
82
|
+
let reason = 'relevant';
|
|
83
|
+
if (!terms.length) reason = 'generic_prompt';
|
|
84
|
+
else if (!matchedTerms.length) reason = 'no_match';
|
|
85
|
+
else if (!identifierMatches.length && independentTerms.length < 2) reason = 'insufficient_keywords';
|
|
86
|
+
else if (!identifierMatches.length && coverage < 0.5) reason = 'low_coverage';
|
|
87
|
+
else if (!identifierMatches.length && distinctiveness < 0.15) reason = 'low_distinctiveness';
|
|
88
|
+
const score = reason === 'relevant'
|
|
89
|
+
? Math.round((independentTerms.length * 2 + identifierMatches.length * 4 + coverage * 2 + distinctiveness) * 100) / 100
|
|
90
|
+
: 0;
|
|
91
|
+
return { score, matchedTerms, reason, coverage, distinctiveness, identifierMatches, sampleSize };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const RECALL_LIMITS = Object.freeze({
|
|
95
|
+
maxTerms: MAX_TERMS,
|
|
96
|
+
maxCandidates: 128,
|
|
97
|
+
candidatesPerTerm: 32,
|
|
98
|
+
shortTermRows: 512,
|
|
99
|
+
maxBoundaryRows: 4096,
|
|
100
|
+
maxTurnRows: 64,
|
|
101
|
+
maxTurnBytes: 64 * 1024,
|
|
102
|
+
maxReadBytes: 256 * 1024,
|
|
103
|
+
});
|
|
@@ -54,8 +54,9 @@ function recordMessageScan(telemetry) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function withSource(msg, source) {
|
|
57
|
+
const { providerState, thinkingBlocks, ...publicMessage } = msg;
|
|
57
58
|
return {
|
|
58
|
-
...
|
|
59
|
+
...publicMessage,
|
|
59
60
|
content: searchableContent(msg),
|
|
60
61
|
sessionId: msg.sessionId || source.sessionId || null,
|
|
61
62
|
historySource: source.kind,
|
package/yeaft/debug-trace.js
CHANGED
|
@@ -621,6 +621,8 @@ function expandTrace(trace) {
|
|
|
621
621
|
const turnsById = new Map([[trace.requestId || trace.traceId, summarizeTrace(trace, true)]]);
|
|
622
622
|
let snapshot = null;
|
|
623
623
|
let rawRequest = trace?.baseRequest?.rawRequest ?? null;
|
|
624
|
+
let latestRawRequestLoop = null;
|
|
625
|
+
let latestSystemPromptLoop = null;
|
|
624
626
|
const loops = [];
|
|
625
627
|
for (const loop of Array.isArray(trace?.loops) ? trace.loops : []) {
|
|
626
628
|
snapshot = applyRequestDelta(snapshot || trace.baseRequest || null, loop.requestDelta || {});
|
|
@@ -633,8 +635,8 @@ function expandTrace(trace) {
|
|
|
633
635
|
loopInstanceId: loop.loopInstanceId || loop.turnRowId || null,
|
|
634
636
|
loopNumber: loop.loopNumber || 0,
|
|
635
637
|
model: loop.model || null,
|
|
636
|
-
//
|
|
637
|
-
//
|
|
638
|
+
// Keep only the latest available raw request and system prompt, each on
|
|
639
|
+
// its actual source loop. Messages remain limited to the final loop.
|
|
638
640
|
systemPrompt: '',
|
|
639
641
|
messages: [],
|
|
640
642
|
response: loop.response || '',
|
|
@@ -650,12 +652,21 @@ function expandTrace(trace) {
|
|
|
650
652
|
vpId: trace.vpId || null,
|
|
651
653
|
threadId: trace.threadId || null,
|
|
652
654
|
});
|
|
655
|
+
const current = loops.at(-1);
|
|
656
|
+
if (rawRequest != null) {
|
|
657
|
+
if (latestRawRequestLoop) latestRawRequestLoop.rawRequest = null;
|
|
658
|
+
current.rawRequest = rawRequest;
|
|
659
|
+
latestRawRequestLoop = current;
|
|
660
|
+
}
|
|
661
|
+
if (snapshot.systemPrompt) {
|
|
662
|
+
if (latestSystemPromptLoop) latestSystemPromptLoop.systemPrompt = '';
|
|
663
|
+
current.systemPrompt = snapshot.systemPrompt;
|
|
664
|
+
latestSystemPromptLoop = current;
|
|
665
|
+
}
|
|
653
666
|
}
|
|
654
667
|
const latest = loops.at(-1);
|
|
655
668
|
if (latest && snapshot) {
|
|
656
|
-
latest.systemPrompt = snapshot.systemPrompt || '';
|
|
657
669
|
latest.messages = Array.isArray(snapshot.messages) ? snapshot.messages : [];
|
|
658
|
-
latest.rawRequest = rawRequest;
|
|
659
670
|
}
|
|
660
671
|
return { loops, turns: Array.from(turnsById.values()) };
|
|
661
672
|
}
|
package/yeaft/effort.js
CHANGED
|
@@ -14,14 +14,15 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Red lines:
|
|
16
16
|
* • Never error on unknown scenario — default to 'max'.
|
|
17
|
-
* •
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* module does NOT consult the capability matrix.
|
|
17
|
+
* • The ordinary picker preserves existing scenario defaults. Child effort
|
|
18
|
+
* is separately constrained by the capability-aware final payload helpers.
|
|
19
|
+
* • Child ceilings cannot be disabled by YEAFT_THINKING_V1, user overrides,
|
|
20
|
+
* routing, nesting, or extraBody. Unsupported models omit effort fields.
|
|
22
21
|
*/
|
|
23
22
|
|
|
24
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
normalizeEffort, getThinkingCapability, getModelEffortOptions, thinkingBudgetForEffort,
|
|
25
|
+
} from './models.js';
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Number of tool-loop turns past which a query is considered "complex"
|
|
@@ -116,3 +117,155 @@ export function parseEffortPrefix(prompt) {
|
|
|
116
117
|
const cleanedPrompt = prompt.slice(m[0].length);
|
|
117
118
|
return { effort, cleanedPrompt };
|
|
118
119
|
}
|
|
120
|
+
|
|
121
|
+
// Ordinal levels, not lexical sorting or cross-model token-budget equivalence.
|
|
122
|
+
export const EFFORT_LEVELS = Object.freeze(['minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
|
|
123
|
+
const effortRank = value => EFFORT_LEVELS.indexOf(normalizeEffort(value));
|
|
124
|
+
const atMost = (value, ceiling) => effortRank(value) >= 0 && effortRank(value) <= effortRank(ceiling);
|
|
125
|
+
|
|
126
|
+
/** Copy only durable decision fields; never retain a mutable request/config object. */
|
|
127
|
+
export function snapshotEffortDecision(decision = null) {
|
|
128
|
+
return Object.freeze({
|
|
129
|
+
requested: normalizeEffort(decision?.requested),
|
|
130
|
+
effective: normalizeEffort(decision?.effective),
|
|
131
|
+
source: typeof decision?.source === 'string' ? decision.source : 'unknown',
|
|
132
|
+
model: typeof decision?.model === 'string' ? decision.model : null,
|
|
133
|
+
wireMode: typeof decision?.wireMode === 'string' ? decision.wireMode : 'omitted',
|
|
134
|
+
thinkingEnabled: decision?.thinkingEnabled === true,
|
|
135
|
+
cap: normalizeEffort(decision?.cap),
|
|
136
|
+
...(Number.isFinite(decision?.budgetTokens) ? { budgetTokens: decision.budgetTokens } : {}),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Capture the decision belonging to the provider response that generated a tool call. */
|
|
141
|
+
export function captureParentEffortDecision(ctx = {}) {
|
|
142
|
+
return snapshotEffortDecision(ctx.effortDecision ?? ctx.parentEngineDeps?.effortDecision);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function effortError(model, target, detail = '') {
|
|
146
|
+
const error = new Error(`Sub-agent model "${model}" cannot express effort <= ${target}${detail ? ` (${detail})` : ''}. Select a compatible child model.`);
|
|
147
|
+
error.code = 'SUB_AGENT_EFFORT_UNREPRESENTABLE';
|
|
148
|
+
return error;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function capabilityContext(protocol, effortContext) {
|
|
152
|
+
return { ...effortContext, protocol: protocol || effortContext?.protocol };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve the non-disableable child ceiling using the actual model's capabilities.
|
|
157
|
+
* Unknown/omitted parent wire defaults are conservative medium, never chat/max.
|
|
158
|
+
*/
|
|
159
|
+
export function resolveSubAgentEffort({ parentDecision = null, model, effortContext = {}, protocol } = {}) {
|
|
160
|
+
const parent = snapshotEffortDecision(parentDecision);
|
|
161
|
+
// An unsupported intermediate model has no wire effort, but must not erase
|
|
162
|
+
// an inherited lower ceiling when it delegates again.
|
|
163
|
+
let parentTarget = parent.effective || 'medium';
|
|
164
|
+
if (parent.cap && atMost(parent.cap, parentTarget)) parentTarget = parent.cap;
|
|
165
|
+
const target = atMost(parentTarget, 'high') ? parentTarget : 'high';
|
|
166
|
+
const context = capabilityContext(protocol, effortContext);
|
|
167
|
+
const capability = getThinkingCapability(model, context);
|
|
168
|
+
const base = {
|
|
169
|
+
requested: parentTarget, source: parent.effective ? 'inherited' : 'fallback',
|
|
170
|
+
model, cap: target, thinkingEnabled: false,
|
|
171
|
+
};
|
|
172
|
+
if (!capability.supportsThinking || capability.thinkingProtocol === 'none') {
|
|
173
|
+
return snapshotEffortDecision({ ...base, effective: null, wireMode: 'unsupported' });
|
|
174
|
+
}
|
|
175
|
+
const supported = getModelEffortOptions(model, context).filter(value => atMost(value, target));
|
|
176
|
+
const effective = EFFORT_LEVELS.filter(value => supported.includes(value)).at(-1);
|
|
177
|
+
if (!effective) throw effortError(model, target);
|
|
178
|
+
const wireMode = protocol === 'openai-responses' || capability.thinkingProtocol === 'openai-reasoning'
|
|
179
|
+
? 'reasoning-effort'
|
|
180
|
+
: capability.thinkingProtocol === 'anthropic-adaptive' ? 'adaptive' : 'manual';
|
|
181
|
+
return snapshotEffortDecision({ ...base, effective, wireMode, thinkingEnabled: true });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function manualEffortForBudget(model, budget) {
|
|
185
|
+
if (!Number.isFinite(budget) || budget <= 0) return null;
|
|
186
|
+
// Round upward: a nonstandard budget must never masquerade as a lower tier.
|
|
187
|
+
return ['low', 'medium', 'high', 'max'].find(level => budget <= thinkingBudgetForEffort(model, level)) || 'ultra';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Read the FINAL wire payload. requested is observability only, not effective.
|
|
192
|
+
* Call after all extraBody/feature-flag/mapping changes, before serialization.
|
|
193
|
+
*/
|
|
194
|
+
export function captureEffortDecision({ body = {}, model, protocol, effortContext = {}, requested = null, source = 'scenario' } = {}) {
|
|
195
|
+
const context = capabilityContext(protocol, effortContext);
|
|
196
|
+
const capability = getThinkingCapability(model, context);
|
|
197
|
+
const base = { requested, source, model, cap: null, thinkingEnabled: false };
|
|
198
|
+
if (!capability.supportsThinking || capability.thinkingProtocol === 'none') {
|
|
199
|
+
return snapshotEffortDecision({ ...base, effective: null, wireMode: 'unsupported' });
|
|
200
|
+
}
|
|
201
|
+
if (protocol === 'openai-responses') {
|
|
202
|
+
const effective = normalizeEffort(body.reasoning?.effort);
|
|
203
|
+
if (effective) return snapshotEffortDecision({ ...base, effective, wireMode: 'reasoning-effort', thinkingEnabled: true });
|
|
204
|
+
} else if (body.thinking?.type === 'enabled') {
|
|
205
|
+
const budgetTokens = body.thinking.budget_tokens;
|
|
206
|
+
return snapshotEffortDecision({ ...base, effective: manualEffortForBudget(model, budgetTokens), wireMode: 'manual', thinkingEnabled: true, budgetTokens });
|
|
207
|
+
} else if (body.thinking?.type === 'adaptive') {
|
|
208
|
+
const effective = normalizeEffort(body.output_config?.effort);
|
|
209
|
+
if (effective) return snapshotEffortDecision({ ...base, effective, wireMode: 'adaptive', thinkingEnabled: true });
|
|
210
|
+
}
|
|
211
|
+
const modelDefault = body.thinking?.type === 'disabled' ? null : normalizeEffort(capability.defaultEffort);
|
|
212
|
+
return snapshotEffortDecision({ ...base, effective: modelDefault, source: modelDefault ? 'model-default' : source, wireMode: 'omitted' });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function removeEffortField(body, key) {
|
|
216
|
+
if (!body[key] || typeof body[key] !== 'object' || Array.isArray(body[key])) {
|
|
217
|
+
delete body[key];
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const { effort: _effort, ...rest } = body[key];
|
|
221
|
+
if (Object.keys(rest).length) body[key] = rest;
|
|
222
|
+
else delete body[key];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Mutate the FINAL provider body in place and return its immutable child decision.
|
|
227
|
+
* The router must preserve effortConstraint even when YEAFT_THINKING_V1 is off.
|
|
228
|
+
* All adapter stream/call paths invoke this AFTER extraBody, BEFORE fetch, and
|
|
229
|
+
* must not subsequently rewrite reasoning/thinking/output_config/max_tokens.
|
|
230
|
+
* @param {object} body Final body owned by the adapter (never caller config).
|
|
231
|
+
* @param {{ model: string, protocol: string, effortContext?: object,
|
|
232
|
+
* effortConstraint: { parentDecision: object|null } }} options
|
|
233
|
+
*/
|
|
234
|
+
export function enforceSubAgentEffortPayload(body, { model, protocol, effortContext = {}, effortConstraint } = {}) {
|
|
235
|
+
if (!effortConstraint) return captureEffortDecision({ body, model, protocol, effortContext });
|
|
236
|
+
const decision = resolveSubAgentEffort({ parentDecision: effortConstraint.parentDecision, model, protocol, effortContext });
|
|
237
|
+
if (decision.wireMode === 'unsupported') {
|
|
238
|
+
removeEffortField(body, 'reasoning');
|
|
239
|
+
removeEffortField(body, 'output_config');
|
|
240
|
+
delete body.thinking;
|
|
241
|
+
return decision;
|
|
242
|
+
}
|
|
243
|
+
const context = capabilityContext(protocol, effortContext);
|
|
244
|
+
const options = getModelEffortOptions(model, context);
|
|
245
|
+
const wireEffort = protocol === 'openai-responses' ? body.reasoning?.effort : body.output_config?.effort;
|
|
246
|
+
const effective = options.includes(wireEffort) && atMost(wireEffort, decision.effective)
|
|
247
|
+
? wireEffort : decision.effective;
|
|
248
|
+
if (protocol === 'openai-responses') {
|
|
249
|
+
body.reasoning = { ...(body.reasoning && typeof body.reasoning === 'object' && !Array.isArray(body.reasoning) ? body.reasoning : {}), effort: effective };
|
|
250
|
+
delete body.thinking;
|
|
251
|
+
removeEffortField(body, 'output_config');
|
|
252
|
+
} else if (decision.wireMode === 'adaptive') {
|
|
253
|
+
body.thinking = { type: 'adaptive' };
|
|
254
|
+
body.output_config = { ...(body.output_config && typeof body.output_config === 'object' && !Array.isArray(body.output_config) ? body.output_config : {}), effort: effective };
|
|
255
|
+
removeEffortField(body, 'reasoning');
|
|
256
|
+
} else {
|
|
257
|
+
const capability = getThinkingCapability(model, context);
|
|
258
|
+
let budget = thinkingBudgetForEffort(model, effective);
|
|
259
|
+
if (!budget) throw effortError(model, decision.cap, 'no manual thinking budget');
|
|
260
|
+
if (Number.isFinite(capability.maxBudgetTokens)) budget = Math.min(budget, capability.maxBudgetTokens);
|
|
261
|
+
const supplied = body.thinking?.type === 'enabled' ? body.thinking.budget_tokens : null;
|
|
262
|
+
if (Number.isInteger(supplied) && supplied >= 1024) budget = Math.min(budget, supplied);
|
|
263
|
+
if (Number.isFinite(body.max_tokens)) budget = Math.min(budget, Math.floor(body.max_tokens) - 1);
|
|
264
|
+
if (budget < 1024) throw effortError(model, decision.cap, 'max_tokens must allow at least 1024 thinking tokens');
|
|
265
|
+
body.thinking = { type: 'enabled', budget_tokens: budget };
|
|
266
|
+
removeEffortField(body, 'output_config');
|
|
267
|
+
removeEffortField(body, 'reasoning');
|
|
268
|
+
return snapshotEffortDecision({ ...decision, effective: manualEffortForBudget(model, budget), budgetTokens: budget });
|
|
269
|
+
}
|
|
270
|
+
return snapshotEffortDecision({ ...decision, effective });
|
|
271
|
+
}
|