@yeaft/webchat-agent 0.1.1004 → 0.1.1006

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.1004",
3
+ "version": "0.1.1006",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/yeaft/config.js CHANGED
@@ -22,7 +22,7 @@
22
22
  import { existsSync, readFileSync } from 'fs';
23
23
  import { join } from 'path';
24
24
  import { DEFAULT_YEAFT_DIR } from './init.js';
25
- import { getModelEffortOptions, modelSupportsEffort, resolveModel, parseModelRef, normalizeProviderModels, resolveContextWindow, resolveMaxOutputTokens } from './models.js';
25
+ import { getModelEffortOptions, getThinkingCapability, modelSupportsEffort, resolveModel, parseModelRef, normalizeProviderModels, resolveContextWindow, resolveMaxOutputTokens } from './models.js';
26
26
  import { normalizeKnownProviderForRuntime } from './llm/known-providers.js';
27
27
 
28
28
  /** Default configuration values. */
@@ -402,8 +402,10 @@ export function loadConfig(overrides = {}) {
402
402
  if (m.maxOutput !== undefined) entry.maxOutput = m.maxOutput;
403
403
  const effortOptions = getModelEffortOptions(m.id);
404
404
  if (effortOptions.length > 0) {
405
+ const cap = getThinkingCapability(m.id);
405
406
  entry.supportsEffort = modelSupportsEffort(m.id);
406
407
  entry.effortOptions = effortOptions;
408
+ entry.effortProtocol = cap.thinkingProtocol;
407
409
  }
408
410
  config.availableModels.push(entry);
409
411
  }
package/yeaft/effort.js CHANGED
@@ -61,7 +61,7 @@ export const SCENARIO_EFFORT = Object.freeze({
61
61
  * Pick the effort level for a given query context.
62
62
  *
63
63
  * Decision order:
64
- * 1. If userEffort is a valid Effort ('minimal'|'low'|'medium'|'high'|'max'),
64
+ * 1. If userEffort is a valid Effort ('minimal'|'low'|'medium'|'high'|'xhigh'|'max'),
65
65
  * return it unchanged. This is the explicit override path —
66
66
  * `/max` prefix, Settings slider, or API caller.
67
67
  * 2. If toolLoopTurns >= LONG_LOOP_TURN_THRESHOLD, upgrade the
@@ -74,7 +74,7 @@ export const SCENARIO_EFFORT = Object.freeze({
74
74
  * already consumed in the current `query()` call.
75
75
  * @param {unknown} [ctx.userEffort=null] — User-supplied override.
76
76
  * Invalid values are ignored (fall through to scenario path).
77
- * @returns {'minimal'|'low'|'medium'|'high'|'max'} Resolved effort. Never null —
77
+ * @returns {'minimal'|'low'|'medium'|'high'|'xhigh'|'max'} Resolved effort. Never null —
78
78
  * the adapter/router is responsible for dropping it when the
79
79
  * feature flag is off or the model doesn't support thinking.
80
80
  */
@@ -97,7 +97,7 @@ export function pickEffort({ scenario = 'chat', toolLoopTurns = 0, userEffort =
97
97
  }
98
98
 
99
99
  /**
100
- * Parse a user prompt for `/max`, `/high`, `/medium`, `/low` prefix
100
+ * Parse a user prompt for `/max`, `/xhigh`, `/high`, `/medium`, `/low` prefix
101
101
  * commands. Returns `{ effort, cleanedPrompt }` where cleanedPrompt has
102
102
  * the prefix (plus one trailing space) stripped.
103
103
  *
@@ -108,11 +108,11 @@ export function pickEffort({ scenario = 'chat', toolLoopTurns = 0, userEffort =
108
108
  * via `!` or `/skill:` instead to avoid collision.
109
109
  *
110
110
  * @param {string} prompt
111
- * @returns {{ effort: 'low'|'medium'|'high'|'max'|null, cleanedPrompt: string }}
111
+ * @returns {{ effort: 'low'|'medium'|'high'|'xhigh'|'max'|null, cleanedPrompt: string }}
112
112
  */
113
113
  export function parseEffortPrefix(prompt) {
114
114
  if (typeof prompt !== 'string') return { effort: null, cleanedPrompt: prompt };
115
- const m = prompt.match(/^\/(max|high|medium|low)(\s+|$)/);
115
+ const m = prompt.match(/^\/(max|xhigh|high|medium|low)(\s+|$)/);
116
116
  if (!m) return { effort: null, cleanedPrompt: prompt };
117
117
  const effort = m[1];
118
118
  const cleanedPrompt = prompt.slice(m[0].length);
@@ -20,6 +20,7 @@ import {
20
20
  normalizeEffort,
21
21
  thinkingBudgetForEffort,
22
22
  getThinkingCapability,
23
+ getModelEffortOptions,
23
24
  } from '../models.js';
24
25
 
25
26
  /**
@@ -30,6 +31,28 @@ function thinkingV1Enabled() {
30
31
  return process.env.YEAFT_THINKING_V1 === '1';
31
32
  }
32
33
 
34
+ function applyAnthropicThinking(body, model, effort) {
35
+ const cap = getThinkingCapability(model);
36
+ if (!cap.supportsThinking) return;
37
+ if (!getModelEffortOptions(model).includes(effort)) return;
38
+
39
+ if (cap.thinkingProtocol === 'anthropic-adaptive') {
40
+ body.thinking = { type: 'adaptive' };
41
+ body.output_config = { ...(body.output_config || {}), effort };
42
+ return;
43
+ }
44
+
45
+ if (cap.thinkingProtocol === 'anthropic') {
46
+ const budget = thinkingBudgetForEffort(model, effort);
47
+ if (budget && budget > 0) {
48
+ // Anthropic manual thinking requires max_tokens > budget_tokens.
49
+ const minMax = budget + 1024;
50
+ if (body.max_tokens < minMax) body.max_tokens = minMax;
51
+ body.thinking = { type: 'enabled', budget_tokens: budget };
52
+ }
53
+ }
54
+ }
55
+
33
56
  const DEFAULT_BASE_URL = 'https://api.anthropic.com';
34
57
  const API_VERSION = '2023-06-01';
35
58
 
@@ -156,7 +179,7 @@ export class AnthropicAdapter extends LLMAdapter {
156
179
  }
157
180
 
158
181
  /**
159
- * @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', effortSource?: 'user'|'auto', signal?: AbortSignal }} params
182
+ * @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
160
183
  * @returns {AsyncGenerator<import('./adapter.js').StreamEvent>}
161
184
  */
162
185
  async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, signal, onRawExchange }) {
@@ -170,24 +193,12 @@ export class AnthropicAdapter extends LLMAdapter {
170
193
  stream: true,
171
194
  };
172
195
 
173
- // task-327a: inject extended-thinking only when feature flag on, effort is
174
- // a valid value, and the model's registry entry says it supports the
175
- // 'anthropic' thinking protocol. Unknown models or non-thinking models
176
- // silently drop the parameter — red line: never error on unsupported.
196
+ // Inject Anthropic thinking only for model-supported effort values.
197
+ // Adaptive Claude 4.7/4.8 uses output_config.effort; older manual-thinking
198
+ // models use budget_tokens. Unsupported combinations silently drop effort.
177
199
  const normEffort = normalizeEffort(effort);
178
200
  if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
179
- const cap = getThinkingCapability(model);
180
- if (cap.supportsThinking && cap.thinkingProtocol === 'anthropic') {
181
- const budget = thinkingBudgetForEffort(model, normEffort);
182
- if (budget && budget > 0) {
183
- // Anthropic requires max_tokens > budget_tokens. Widen max_tokens
184
- // if the caller's value is too small to fit the thinking budget
185
- // plus a sane reply margin (1024 tokens).
186
- const minMax = budget + 1024;
187
- if (body.max_tokens < minMax) body.max_tokens = minMax;
188
- body.thinking = { type: 'enabled', budget_tokens: budget };
189
- }
190
- }
201
+ applyAnthropicThinking(body, model, normEffort);
191
202
  }
192
203
 
193
204
  const translatedTools = this.#translateTools(tools);
@@ -440,15 +451,7 @@ export class AnthropicAdapter extends LLMAdapter {
440
451
  // task-327c: mirror stream()'s thinking injection for side queries.
441
452
  const normEffort = normalizeEffort(effort);
442
453
  if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
443
- const cap = getThinkingCapability(model);
444
- if (cap.supportsThinking && cap.thinkingProtocol === 'anthropic') {
445
- const budget = thinkingBudgetForEffort(model, normEffort);
446
- if (budget && budget > 0) {
447
- const minMax = budget + 1024;
448
- if (body.max_tokens < minMax) body.max_tokens = minMax;
449
- body.thinking = { type: 'enabled', budget_tokens: budget };
450
- }
451
- }
454
+ applyAnthropicThinking(body, model, normEffort);
452
455
  }
453
456
 
454
457
  const response = await fetch(`${this.#baseUrl}/v1/messages`, {
@@ -38,6 +38,7 @@ import {
38
38
  import {
39
39
  normalizeEffort,
40
40
  getThinkingCapability,
41
+ mapEffortToOpenAIReasoning,
41
42
  } from '../models.js';
42
43
 
43
44
  const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
@@ -52,15 +53,13 @@ function thinkingV1Enabled() {
52
53
  }
53
54
 
54
55
  /**
55
- * Translate a normalised effort ('minimal'|'low'|'medium'|'high'|'max') into the value
56
+ * Translate a normalised effort ('minimal'|'low'|'medium'|'high'|'xhigh'|'max') into the value
56
57
  * accepted by the OpenAI Responses `reasoning.effort` field. Responses today
57
- * accepts 'minimal'|'low'|'medium'|'high' 'max' degrades to 'high' to match the
58
- * registry's normaliseEffort downgrade rule.
58
+ * accepts 'minimal'|'low'|'medium'|'high'. Unsupported Anthropic-only
59
+ * adaptive efforts must be dropped, not downgraded.
59
60
  */
60
61
  function effortForResponses(effort) {
61
- if (!effort) return null;
62
- if (effort === 'max') return 'high';
63
- return effort;
62
+ return mapEffortToOpenAIReasoning(effort);
64
63
  }
65
64
 
66
65
  export class OpenAIResponsesAdapter extends LLMAdapter {
@@ -230,7 +229,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
230
229
  // ─── Streaming ──────────────────────────────────────────
231
230
 
232
231
  /**
233
- * @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', effortSource?: 'user'|'auto', extraBody?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
232
+ * @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'minimal'|'low'|'medium'|'high'|'xhigh'|'max', effortSource?: 'user'|'auto', extraBody?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
234
233
  *
235
234
  * NOTE on `extraBody`: any keys you spread here are merged verbatim into
236
235
  * the wire body and — because the verbatim debug feature is intentionally
@@ -115,7 +115,7 @@ export function filterEffortForModel(params) {
115
115
  const { effort: _drop, effortSource: _source, ...rest } = params;
116
116
  return rest;
117
117
  }
118
- if (norm === 'minimal' && !getModelEffortOptions(modelId).includes('minimal')) {
118
+ if (!getModelEffortOptions(modelId).includes(norm)) {
119
119
  const { effort: _drop, effortSource: _source, ...rest } = params;
120
120
  return rest;
121
121
  }
package/yeaft/models.js CHANGED
@@ -31,9 +31,10 @@ import { lookupModelLimitSync } from './llm/models-dev.js';
31
31
  * @property {string} baseUrl — Official API endpoint base URL
32
32
  * @property {string} displayName — Human-readable model name
33
33
  * @property {boolean} [supportsThinking] — task-327a: model supports thinking/reasoning effort.
34
- * @property {'anthropic' | 'openai-reasoning' | 'none'} [thinkingProtocol] — task-327a:
34
+ * @property {'anthropic' | 'anthropic-adaptive' | 'openai-reasoning' | 'none'} [thinkingProtocol] — task-327a:
35
35
  * 'anthropic' → thinking:{type:'enabled', budget_tokens:N}
36
- * 'openai-reasoning' → reasoning:{effort:'low'|'medium'|'high'}
36
+ * 'anthropic-adaptive' → thinking:{type:'adaptive'} + output_config:{effort}
37
+ * 'openai-reasoning' → reasoning:{effort:'minimal'|'low'|'medium'|'high'}
37
38
  * 'none' (default) → parameter silently dropped by router
38
39
  * @property {'low' | 'medium' | 'high' | 'max' | null} [defaultEffort] — task-327a: adapter-level default
39
40
  * when caller doesn't specify effort (null = no default / decision-tree decides).
@@ -73,8 +74,9 @@ export const MODEL_REGISTRY = new Map([
73
74
  baseUrl: 'https://api.anthropic.com',
74
75
  displayName: 'Claude Opus 4.8',
75
76
  supportsThinking: true,
76
- thinkingProtocol: 'anthropic',
77
- defaultEffort: null,
77
+ thinkingProtocol: 'anthropic-adaptive',
78
+ defaultEffort: 'high',
79
+ effortOptions: ['low', 'medium', 'high', 'xhigh', 'max'],
78
80
  maxBudgetTokens: 64000,
79
81
  }],
80
82
  ['claude-opus-4.8', {
@@ -83,8 +85,9 @@ export const MODEL_REGISTRY = new Map([
83
85
  baseUrl: 'https://api.anthropic.com',
84
86
  displayName: 'Claude Opus 4.8',
85
87
  supportsThinking: true,
86
- thinkingProtocol: 'anthropic',
87
- defaultEffort: null,
88
+ thinkingProtocol: 'anthropic-adaptive',
89
+ defaultEffort: 'high',
90
+ effortOptions: ['low', 'medium', 'high', 'xhigh', 'max'],
88
91
  maxBudgetTokens: 64000,
89
92
  }],
90
93
  ['claude-haiku-3-20250414', {
@@ -383,7 +386,7 @@ export function parseModelRef(ref) {
383
386
 
384
387
  /**
385
388
  * Valid effort levels accepted by Yeaft adapters.
386
- * @typedef {'minimal' | 'low' | 'medium' | 'high' | 'max'} Effort
389
+ * @typedef {'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'} Effort
387
390
  */
388
391
 
389
392
  /**
@@ -417,15 +420,14 @@ export function mapEffortToOpenAIReasoning(effort) {
417
420
  case 'low': return 'low';
418
421
  case 'medium': return 'medium';
419
422
  case 'high': return 'high';
420
- // OpenAI doesn't support 'max'; degrade to 'high'. Engine may emit a
421
- // debug line noting the downgrade — adapter level stays silent.
422
- case 'max': return 'high';
423
423
  default: return null;
424
424
  }
425
425
  }
426
426
 
427
427
  export const OPENAI_REASONING_EFFORT_OPTIONS = ['minimal', 'low', 'medium', 'high'];
428
- export const ANTHROPIC_EFFORT_OPTIONS = ['low', 'medium', 'high'];
428
+ export const ANTHROPIC_MANUAL_EFFORT_OPTIONS = ['low', 'medium', 'high'];
429
+ export const ANTHROPIC_ADAPTIVE_EFFORT_OPTIONS = ['low', 'medium', 'high', 'xhigh', 'max'];
430
+ export const ANTHROPIC_ADAPTIVE_MAX_EFFORT_OPTIONS = ['low', 'medium', 'high', 'max'];
429
431
 
430
432
  function inferThinkingCapability(model) {
431
433
  const id = parseModelRef(model).modelId.toLowerCase();
@@ -435,9 +437,32 @@ function inferThinkingCapability(model) {
435
437
  return { supportsThinking: true, thinkingProtocol: 'openai-reasoning', defaultEffort: null, maxBudgetTokens: null };
436
438
  }
437
439
 
440
+ // Claude Opus 4.7/4.8 expose the full adaptive effort set, including xhigh.
441
+ if (/^claude-opus-4[-.]?(7|8)($|-|\.)/.test(id)) {
442
+ return {
443
+ supportsThinking: true,
444
+ thinkingProtocol: 'anthropic-adaptive',
445
+ defaultEffort: 'high',
446
+ maxBudgetTokens: null,
447
+ effortOptions: ANTHROPIC_ADAPTIVE_EFFORT_OPTIONS,
448
+ };
449
+ }
450
+
451
+ // Claude Opus 4.6 and Sonnet 4.6 use adaptive effort too, but official
452
+ // Anthropic docs do not list xhigh for them. Keep max, reject xhigh.
453
+ if (/^claude-(opus|sonnet)-4[-.]?6($|-|\.)/.test(id)) {
454
+ return {
455
+ supportsThinking: true,
456
+ thinkingProtocol: 'anthropic-adaptive',
457
+ defaultEffort: 'high',
458
+ maxBudgetTokens: null,
459
+ effortOptions: ANTHROPIC_ADAPTIVE_MAX_EFFORT_OPTIONS,
460
+ };
461
+ }
462
+
438
463
  // Anthropic extended thinking is available on Claude 3.7+ and Claude 4.x.
439
464
  // Be conservative: older Claude 3/3.5/Haiku entries stay unsupported unless
440
- // explicitly listed in the registry.
465
+ // explicitly listed in the registry. Older Claude 4.x entries keep manual budgets.
441
466
  if (/^claude-/.test(id) && (/(^|-)3-7($|-|\.)/.test(id) || /(^|-)4($|-|\.)/.test(id))) {
442
467
  const maxBudgetTokens = id.includes('opus') ? 64000 : 32000;
443
468
  return { supportsThinking: true, thinkingProtocol: 'anthropic', defaultEffort: null, maxBudgetTokens };
@@ -474,7 +499,7 @@ export function thinkingBudgetForEffort(model, effort) {
474
499
  * models (red line: never error on unsupported).
475
500
  *
476
501
  * @param {string} model
477
- * @returns {{ supportsThinking: boolean, thinkingProtocol: 'anthropic' | 'openai-reasoning' | 'none', defaultEffort: Effort | null, maxBudgetTokens: number | null }}
502
+ * @returns {{ supportsThinking: boolean, thinkingProtocol: 'anthropic' | 'anthropic-adaptive' | 'openai-reasoning' | 'none', defaultEffort: Effort | null, maxBudgetTokens: number | null, effortOptions: Effort[] }}
478
503
  */
479
504
  export function getThinkingCapability(model) {
480
505
  const info = MODEL_REGISTRY.get(model);
@@ -486,6 +511,7 @@ export function getThinkingCapability(model) {
486
511
  thinkingProtocol: 'none',
487
512
  defaultEffort: null,
488
513
  maxBudgetTokens: null,
514
+ effortOptions: [],
489
515
  };
490
516
  }
491
517
  return {
@@ -493,14 +519,17 @@ export function getThinkingCapability(model) {
493
519
  thinkingProtocol: info?.thinkingProtocol || inferred?.thinkingProtocol || 'none',
494
520
  defaultEffort: info?.defaultEffort ?? inferred?.defaultEffort ?? null,
495
521
  maxBudgetTokens: info?.maxBudgetTokens ?? inferred?.maxBudgetTokens ?? null,
522
+ effortOptions: (info?.effortOptions || inferred?.effortOptions || null),
496
523
  };
497
524
  }
498
525
 
499
526
  export function getModelEffortOptions(model) {
500
527
  const cap = getThinkingCapability(model);
501
528
  if (!cap.supportsThinking || cap.thinkingProtocol === 'none') return [];
529
+ if (Array.isArray(cap.effortOptions)) return cap.effortOptions.slice();
502
530
  if (cap.thinkingProtocol === 'openai-reasoning') return OPENAI_REASONING_EFFORT_OPTIONS.slice();
503
- if (cap.thinkingProtocol === 'anthropic') return ANTHROPIC_EFFORT_OPTIONS.slice();
531
+ if (cap.thinkingProtocol === 'anthropic-adaptive') return ANTHROPIC_ADAPTIVE_EFFORT_OPTIONS.slice();
532
+ if (cap.thinkingProtocol === 'anthropic') return ANTHROPIC_MANUAL_EFFORT_OPTIONS.slice();
504
533
  return [];
505
534
  }
506
535
 
@@ -515,7 +544,7 @@ export function modelSupportsEffort(model) {
515
544
  * @returns {Effort | null}
516
545
  */
517
546
  export function normalizeEffort(effort) {
518
- if (effort === 'minimal' || effort === 'low' || effort === 'medium' || effort === 'high' || effort === 'max') {
547
+ if (effort === 'minimal' || effort === 'low' || effort === 'medium' || effort === 'high' || effort === 'xhigh' || effort === 'max') {
519
548
  return effort;
520
549
  }
521
550
  return null;
package/yeaft/prompts.js CHANGED
@@ -197,6 +197,7 @@ const PROMPTS = {
197
197
  tools: (names) => `Available tools: ${names}`,
198
198
  // DESIGN-PROMPT §3 ④ — Active Scope header
199
199
  activeScopeHeader: '## active_scope',
200
+ multiVpRoutingHeader: '## multi_vp_routing',
200
201
  sessionAnnouncementHeader: '[Session Announcement]',
201
202
  // Project-doc (CLAUDE.md / AGENTS.md) header + one-liner intro. Both
202
203
  // filenames are recognized: CLAUDE.md is this project's convention,
@@ -212,6 +213,7 @@ const PROMPTS = {
212
213
  tools: (names) => `可用工具:${names}`,
213
214
  // DESIGN-PROMPT §3 ④ — Active Scope header
214
215
  activeScopeHeader: '## active_scope',
216
+ multiVpRoutingHeader: '## multi_vp_routing',
215
217
  sessionAnnouncementHeader: '[会话公告]',
216
218
  // 项目文档块:CLAUDE.md / AGENTS.md(与 Codex 通用命名兼容)。
217
219
  projectDocHeader: '[项目文档]',
@@ -393,6 +395,9 @@ export function buildSystemPrompt({
393
395
  const activeScopeBlock = renderActiveScope(activeScope, lang);
394
396
  if (activeScopeBlock) parts.push(activeScopeBlock);
395
397
 
398
+ const multiVpRoutingBlock = renderMultiVpRouting(activeScope, lang);
399
+ if (multiVpRoutingBlock) parts.push(multiVpRoutingBlock);
400
+
396
401
  return parts.join('\n\n');
397
402
  }
398
403
 
@@ -490,7 +495,11 @@ function firstNonEmptyString(...values) {
490
495
  }
491
496
 
492
497
  function renderSessionMembersLine(members) {
493
- if (!Array.isArray(members)) return '';
498
+ return normalizeSessionMemberIds(members).join(', ');
499
+ }
500
+
501
+ function normalizeSessionMemberIds(members) {
502
+ if (!Array.isArray(members)) return [];
494
503
  const clean = [];
495
504
  const seen = new Set();
496
505
  for (const member of members) {
@@ -500,7 +509,38 @@ function renderSessionMembersLine(members) {
500
509
  seen.add(id);
501
510
  clean.push(id);
502
511
  }
503
- return clean.join(', ');
512
+ return clean;
513
+ }
514
+
515
+ function renderMultiVpRouting(activeScope, lang) {
516
+ if (!activeScope || typeof activeScope !== 'object') return '';
517
+ const ownId = firstNonEmptyString(activeScope.sessionMember, activeScope.vpId);
518
+ const members = normalizeSessionMemberIds(activeScope.sessionMembers || activeScope.members);
519
+ const peers = ownId ? members.filter((member) => member !== ownId) : members;
520
+ if (peers.length === 0) return '';
521
+
522
+ const header = lang.multiVpRoutingHeader || '## multi_vp_routing';
523
+ if (lang === PROMPTS.zh) {
524
+ return [
525
+ header,
526
+ `当前 VP: ${ownId || 'unknown'}`,
527
+ `可转发 VP: ${peers.join(', ')}`,
528
+ '- 多 VP session 中,先主动感知这些 VP 的职责;不要假装只有你一个人在场。',
529
+ '- 当用户点名其他 VP、任务明显属于其他 VP、需要并行协作,或你需要另一个 VP 继续处理时,必须调用 `route_forward`。',
530
+ '- VP 自己写 @mention 不会触发路由;只有 `route_forward` 工具会真正把任务交给目标 VP。',
531
+ '- 如果要多人一起处理,调用 `route_forward`,`to` 可填目标 vpId 或 `all`;`text` 要包含明确任务和必要上下文。',
532
+ ].join('\n');
533
+ }
534
+
535
+ return [
536
+ header,
537
+ `Current VP: ${ownId || 'unknown'}`,
538
+ `Forwardable VPs: ${peers.join(', ')}`,
539
+ '- In a multi-VP session, actively notice these peers and their likely responsibilities; do not behave as if you are alone.',
540
+ '- When the user names another VP, the task clearly belongs to another VP, parallel collaboration is needed, or another VP should continue the work, you MUST call `route_forward`.',
541
+ '- VP-written @mentions do not route anything; only the `route_forward` tool performs a real hand-off.',
542
+ '- For multi-person work, call `route_forward` with a target vpId or `all`; include the concrete task and required context in `text`.',
543
+ ].join('\n');
504
544
  }
505
545
 
506
546
  /**
@@ -28,7 +28,7 @@ const CONFIG_FILE = 'config.json';
28
28
 
29
29
  /** Whitelist of persisted session model-override fields. Reject everything else. */
30
30
  const ALLOWED_KEYS = new Set(['model', 'modelEffort']);
31
- const ALLOWED_EFFORTS = new Set(['minimal', 'low', 'medium', 'high']);
31
+ const ALLOWED_EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
32
32
 
33
33
  export class SessionConfigError extends Error {
34
34
  constructor(code, message) {
@@ -101,7 +101,7 @@ export function validateSessionConfig(cfg) {
101
101
  }
102
102
  if ('modelEffort' in cfg && cfg.modelEffort !== null && cfg.modelEffort !== undefined && cfg.modelEffort !== '') {
103
103
  if (typeof cfg.modelEffort !== 'string' || !ALLOWED_EFFORTS.has(cfg.modelEffort.trim())) {
104
- throw new SessionConfigError('invalid_model_effort', 'modelEffort must be minimal, low, medium, or high');
104
+ throw new SessionConfigError('invalid_model_effort', 'modelEffort must be minimal, low, medium, high, xhigh, or max');
105
105
  }
106
106
  }
107
107
  }
@@ -27,16 +27,21 @@ import { defineTool } from './types.js';
27
27
 
28
28
  export default defineTool({
29
29
  name: 'RouteForward',
30
- description: `Hand this turn off to another VP in the same group.
30
+ description: `Hand this turn off to another VP in the same session.
31
31
 
32
32
  Use this tool — NOT free-text @mentions — to route a question or task to
33
33
  another VP. VP-authored @mentions in chat text are NOT automatically routed
34
- (the group coordinator only text-routes for user messages); you must call
35
- RouteForward for the hand-off to take effect.
34
+ (the coordinator only text-routes user messages); you must call RouteForward
35
+ for the hand-off to take effect.
36
+
37
+ In a multi-VP session, treat RouteForward as the required hand-off mechanism:
38
+ - If the user names another VP, call RouteForward to that vpId.
39
+ - If another VP clearly owns the domain or should continue the work, call RouteForward instead of only mentioning them.
40
+ - If the task needs parallel collaboration, call RouteForward with a target vpId or "all".
36
41
 
37
42
  Arguments:
38
43
  - to (string): target vpId, or the literal "all" to broadcast to every
39
- other member of the group (subject to the per-group fan-out cap).
44
+ other member of the session (subject to the session fan-out cap).
40
45
  - text (string): the message body to send on your behalf.
41
46
  - reason (string, optional): short rationale for the forward, recorded on
42
47
  the message meta for audit / UI display.
@@ -47,7 +52,7 @@ Rules:
47
52
  - Forwards carry a causedBy chain; chains deeper than 10 hops are blocked
48
53
  (chain_depth_exceeded).
49
54
  - A single target may be forwarded to at most 8 times per 5-second window
50
- per group (throttled).
55
+ per session (throttled).
51
56
 
52
57
  Returns JSON: { ok, dispatched?, error?, detail? }.`,
53
58
  parameters: {