@yeaft/webchat-agent 0.1.997 → 0.1.999

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.997",
3
+ "version": "0.1.999",
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 { resolveModel, parseModelRef, normalizeProviderModels, resolveContextWindow, resolveMaxOutputTokens } from './models.js';
25
+ import { getModelEffortOptions, 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. */
@@ -394,11 +394,17 @@ export function loadConfig(overrides = {}) {
394
394
  if (!config.availableModels.some(am => am.id === m.id)) {
395
395
  const entry = {
396
396
  id: m.id,
397
+ ref: p.name ? `${p.name}/${m.id}` : m.id,
397
398
  provider: p.name,
398
399
  label: m.id,
399
400
  };
400
401
  if (m.contextWindow !== undefined) entry.contextWindow = m.contextWindow;
401
402
  if (m.maxOutput !== undefined) entry.maxOutput = m.maxOutput;
403
+ const effortOptions = getModelEffortOptions(m.id);
404
+ if (effortOptions.length > 0) {
405
+ entry.supportsEffort = modelSupportsEffort(m.id);
406
+ entry.effortOptions = effortOptions;
407
+ }
402
408
  config.availableModels.push(entry);
403
409
  }
404
410
  }
@@ -303,6 +303,6 @@ function oneLine(text) {
303
303
 
304
304
  function extractSystem(language) {
305
305
  return String(language || '').toLowerCase().startsWith('zh')
306
- ? '你是 Yeaft Dream 记忆抽取器。只输出严格 JSON 数组,不要 Markdown。保留具体事实、决策、偏好、当前状态和证据 message id。'
307
- : 'You are the Yeaft Dream memory extractor. Return only a strict JSON array, no Markdown. Preserve concrete facts, decisions, preferences, current status, and evidence message ids.';
306
+ ? '你是梦境记忆抽取器。只输出严格 JSON 数组,不要 Markdown。保留具体事实、决策、偏好、当前状态和证据 message id。'
307
+ : 'You are the dream memory extractor. Return only a strict JSON array, no Markdown. Preserve concrete facts, decisions, preferences, current status, and evidence message ids.';
308
308
  }
package/yeaft/engine.js CHANGED
@@ -1372,7 +1372,8 @@ export class Engine {
1372
1372
  // valid prompt prefix.
1373
1373
  const parsed = parseEffortPrefix(prompt);
1374
1374
  const effectivePrompt = parsed.cleanedPrompt;
1375
- const effectiveUserEffort = normalizeEffort(userEffort) || parsed.effort || null;
1375
+ const configuredEffort = normalizeEffort(this.#config?.modelEffort);
1376
+ const effectiveUserEffort = normalizeEffort(userEffort) || parsed.effort || configuredEffort || null;
1376
1377
  const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
1377
1378
  ? collabToolPolicy
1378
1379
  : null;
@@ -1957,6 +1958,7 @@ export class Engine {
1957
1958
  tools: toolDefs.length > 0 ? toolDefs : undefined,
1958
1959
  maxTokens: this.#config.maxOutputTokens || 16384,
1959
1960
  effort: resolvedEffort,
1961
+ effortSource: userEffort ? 'user' : 'auto',
1960
1962
  signal,
1961
1963
  onRawExchange: captureRawExchange,
1962
1964
  })) {
@@ -156,10 +156,10 @@ export class AnthropicAdapter extends LLMAdapter {
156
156
  }
157
157
 
158
158
  /**
159
- * @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', signal?: AbortSignal }} params
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
160
160
  * @returns {AsyncGenerator<import('./adapter.js').StreamEvent>}
161
161
  */
162
- async *stream({ model, system, messages, tools, maxTokens = 16384, effort, signal, onRawExchange }) {
162
+ async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, signal, onRawExchange }) {
163
163
  if (signal?.aborted) throw new LLMAbortError();
164
164
 
165
165
  const body = {
@@ -175,7 +175,7 @@ export class AnthropicAdapter extends LLMAdapter {
175
175
  // 'anthropic' thinking protocol. Unknown models or non-thinking models
176
176
  // silently drop the parameter — red line: never error on unsupported.
177
177
  const normEffort = normalizeEffort(effort);
178
- if (thinkingV1Enabled() && normEffort) {
178
+ if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
179
179
  const cap = getThinkingCapability(model);
180
180
  if (cap.supportsThinking && cap.thinkingProtocol === 'anthropic') {
181
181
  const budget = thinkingBudgetForEffort(model, normEffort);
@@ -427,7 +427,7 @@ export class AnthropicAdapter extends LLMAdapter {
427
427
  * models silently drop the param. max_tokens auto-widens to budget+1024
428
428
  * when needed.
429
429
  */
430
- async call({ model, system, messages, maxTokens = 4096, effort, signal }) {
430
+ async call({ model, system, messages, maxTokens = 4096, effort, effortSource, signal }) {
431
431
  if (signal?.aborted) throw new LLMAbortError();
432
432
 
433
433
  const body = {
@@ -439,7 +439,7 @@ export class AnthropicAdapter extends LLMAdapter {
439
439
 
440
440
  // task-327c: mirror stream()'s thinking injection for side queries.
441
441
  const normEffort = normalizeEffort(effort);
442
- if (thinkingV1Enabled() && normEffort) {
442
+ if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
443
443
  const cap = getThinkingCapability(model);
444
444
  if (cap.supportsThinking && cap.thinkingProtocol === 'anthropic') {
445
445
  const budget = thinkingBudgetForEffort(model, normEffort);
@@ -230,7 +230,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
230
230
  // ─── Streaming ──────────────────────────────────────────
231
231
 
232
232
  /**
233
- * @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', extraBody?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
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
234
234
  *
235
235
  * NOTE on `extraBody`: any keys you spread here are merged verbatim into
236
236
  * the wire body and — because the verbatim debug feature is intentionally
@@ -239,7 +239,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
239
239
  * `api-key` headers are auto-redacted (see `redactRawRequest` in
240
240
  * `adapter.js`); request-body fields are caller-controlled.
241
241
  */
242
- async *stream({ model, system, messages, tools, maxTokens = 16384, effort, extraBody, signal, onRawExchange }) {
242
+ async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, extraBody, signal, onRawExchange }) {
243
243
  if (signal?.aborted) throw new LLMAbortError();
244
244
 
245
245
  const body = {
@@ -257,7 +257,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
257
257
  // registry entry must declare thinkingProtocol === 'openai-reasoning'.
258
258
  // Unknown / unsupported models silently drop the field.
259
259
  const normEffort = normalizeEffort(effort);
260
- if (thinkingV1Enabled() && normEffort) {
260
+ if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
261
261
  const cap = getThinkingCapability(model);
262
262
  if (cap.supportsThinking && cap.thinkingProtocol === 'openai-reasoning') {
263
263
  const wireEffort = effortForResponses(normEffort);
@@ -478,7 +478,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
478
478
  * expose them, mirror the stream() instrumentation. Parity with
479
479
  * anthropic.js's `call()`.
480
480
  */
481
- async call({ model, system, messages, maxTokens = 4096, effort, extraBody, signal }) {
481
+ async call({ model, system, messages, maxTokens = 4096, effort, effortSource, extraBody, signal }) {
482
482
  if (signal?.aborted) throw new LLMAbortError();
483
483
 
484
484
  const body = {
@@ -490,7 +490,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
490
490
 
491
491
  // Mirror stream()'s thinking injection for non-streaming side queries.
492
492
  const normEffort = normalizeEffort(effort);
493
- if (thinkingV1Enabled() && normEffort) {
493
+ if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
494
494
  const cap = getThinkingCapability(model);
495
495
  if (cap.supportsThinking && cap.thinkingProtocol === 'openai-reasoning') {
496
496
  const wireEffort = effortForResponses(normEffort);
@@ -99,20 +99,22 @@ function thinkingV1Enabled() {
99
99
  export function filterEffortForModel(params) {
100
100
  if (!params || !('effort' in params)) return params;
101
101
  if (!thinkingV1Enabled()) {
102
- const { effort: _drop, ...rest } = params;
103
- return rest;
102
+ if (params.effortSource !== 'user') {
103
+ const { effort: _drop, effortSource: _source, ...rest } = params;
104
+ return rest;
105
+ }
104
106
  }
105
107
  const norm = normalizeEffort(params.effort);
106
108
  if (!norm) {
107
- const { effort: _drop, ...rest } = params;
109
+ const { effort: _drop, effortSource: _source, ...rest } = params;
108
110
  return rest;
109
111
  }
110
112
  const cap = getThinkingCapability(parseModelRef(params.model).modelId);
111
113
  if (!cap.supportsThinking || cap.thinkingProtocol === 'none') {
112
- const { effort: _drop, ...rest } = params;
114
+ const { effort: _drop, effortSource: _source, ...rest } = params;
113
115
  return rest;
114
116
  }
115
- return { ...params, effort: norm };
117
+ return { ...params, effort: norm, effortSource: params.effortSource };
116
118
  }
117
119
 
118
120
  /**
package/yeaft/models.js CHANGED
@@ -411,6 +411,27 @@ export function mapEffortToOpenAIReasoning(effort) {
411
411
  }
412
412
  }
413
413
 
414
+ export const MODEL_EFFORT_OPTIONS = ['low', 'medium', 'high'];
415
+
416
+ function inferThinkingCapability(model) {
417
+ const id = parseModelRef(model).modelId.toLowerCase();
418
+ if (!id) return null;
419
+
420
+ if (/^(gpt-5|o1|o3|o4|chatgpt-|codex-)/.test(id)) {
421
+ return { supportsThinking: true, thinkingProtocol: 'openai-reasoning', defaultEffort: null, maxBudgetTokens: null };
422
+ }
423
+
424
+ // Anthropic extended thinking is available on Claude 3.7+ and Claude 4.x.
425
+ // Be conservative: older Claude 3/3.5/Haiku entries stay unsupported unless
426
+ // explicitly listed in the registry.
427
+ if (/^claude-/.test(id) && (/(^|-)3-7($|-|\.)/.test(id) || /(^|-)4($|-|\.)/.test(id))) {
428
+ const maxBudgetTokens = id.includes('opus') ? 64000 : 32000;
429
+ return { supportsThinking: true, thinkingProtocol: 'anthropic', defaultEffort: null, maxBudgetTokens };
430
+ }
431
+
432
+ return null;
433
+ }
434
+
414
435
  /**
415
436
  * Resolve the Anthropic thinking budget_tokens value for a given (model, effort).
416
437
  *
@@ -443,7 +464,9 @@ export function thinkingBudgetForEffort(model, effort) {
443
464
  */
444
465
  export function getThinkingCapability(model) {
445
466
  const info = MODEL_REGISTRY.get(model);
446
- if (!info || !info.supportsThinking) {
467
+ const hasExplicitThinking = info && Object.prototype.hasOwnProperty.call(info, 'supportsThinking');
468
+ const inferred = hasExplicitThinking ? null : inferThinkingCapability(model);
469
+ if ((!info || !info.supportsThinking) && !inferred) {
447
470
  return {
448
471
  supportsThinking: false,
449
472
  thinkingProtocol: 'none',
@@ -452,13 +475,23 @@ export function getThinkingCapability(model) {
452
475
  };
453
476
  }
454
477
  return {
455
- supportsThinking: true,
456
- thinkingProtocol: info.thinkingProtocol || 'none',
457
- defaultEffort: info.defaultEffort ?? null,
458
- maxBudgetTokens: info.maxBudgetTokens ?? null,
478
+ supportsThinking: Boolean(info?.supportsThinking ?? inferred?.supportsThinking),
479
+ thinkingProtocol: info?.thinkingProtocol || inferred?.thinkingProtocol || 'none',
480
+ defaultEffort: info?.defaultEffort ?? inferred?.defaultEffort ?? null,
481
+ maxBudgetTokens: info?.maxBudgetTokens ?? inferred?.maxBudgetTokens ?? null,
459
482
  };
460
483
  }
461
484
 
485
+ export function getModelEffortOptions(model) {
486
+ const cap = getThinkingCapability(model);
487
+ if (!cap.supportsThinking || cap.thinkingProtocol === 'none') return [];
488
+ return MODEL_EFFORT_OPTIONS.slice();
489
+ }
490
+
491
+ export function modelSupportsEffort(model) {
492
+ return getModelEffortOptions(model).length > 0;
493
+ }
494
+
462
495
  /**
463
496
  * Valid-effort guard. Unknown values → null (caller should treat as "no effort").
464
497
  *
package/yeaft/prompts.js CHANGED
@@ -191,7 +191,7 @@ export function getDefaultPlanInstruction(language = 'en') {
191
191
 
192
192
  const PROMPTS = {
193
193
  en: {
194
- identity: 'You are Yeaft, a helpful AI assistant.',
194
+ identity: 'No VP soul is active for this turn. Participate in the current session with grounded, evidence-based answers and preserve the user\'s context.',
195
195
  date: (d) => `Date: ${d}`,
196
196
  dream: 'You are in dream mode. Reflect on past conversations and consolidate memories.',
197
197
  tools: (names) => `Available tools: ${names}`,
@@ -204,11 +204,9 @@ const PROMPTS = {
204
204
  projectDocHeader: '[Project Doc]',
205
205
  projectDocIntro:
206
206
  'The user keeps project-level instructions and context in `CLAUDE.md` or `AGENTS.md` at the session working directory. Treat the content below as authoritative project context — coding conventions, task guidance, workflow rules, etc.',
207
- vpPersonaIntro: (name, role) =>
208
- `You are ${name}${role ? `, ${role}` : ''}. Think, decide, and respond from ${name}'s perspective. Speak in the first person as ${name}; do not refer to yourself as "Yeaft" or as a generic AI assistant.`,
209
207
  },
210
208
  zh: {
211
- identity: '你是 Yeaft,一个有用的 AI 助手。',
209
+ identity: '当前回合没有激活 VP soul。你在当前 session 中参与协作,回答要基于证据,并保持用户上下文。',
212
210
  date: (d) => `日期:${d}`,
213
211
  dream: '你处于梦境模式。回顾过去的对话,整理和巩固记忆。',
214
212
  tools: (names) => `可用工具:${names}`,
@@ -219,8 +217,6 @@ const PROMPTS = {
219
217
  projectDocHeader: '[项目文档]',
220
218
  projectDocIntro:
221
219
  '用户把项目级的说明和上下文记录在 session 工作目录下的 `CLAUDE.md` 或 `AGENTS.md` 中。下面的内容是权威的项目上下文 —— 编码规范、任务指导、工作流约定等,请遵循它来工作。',
222
- vpPersonaIntro: (name, role) =>
223
- `你是 ${name}${role ? `,${role}` : ''}。请以 ${name} 的思考方式理解问题、判断优先级并回答,并以 ${name} 的第一人称发言;不要自称 "Yeaft" 或泛指的 AI 助手。`,
224
220
  },
225
221
  };
226
222
 
@@ -427,7 +423,8 @@ function renderVpPersona(vpPersona, lang, effectiveLang = 'en') {
427
423
  // generic assistant identity here: the VP soul body is the source of truth.
428
424
  // `role` is intentionally not rendered as a second identity line; stock VPs
429
425
  // carry bilingual, role-aware soul text in role.md.
430
- const lines = [`# ${name}`, '', '## Soul'];
426
+ const soulHeading = effectiveLang === 'zh' ? '## 灵魂' : '## Soul';
427
+ const lines = [`# ${name}`, '', soulHeading];
431
428
  if (body) lines.push('', body);
432
429
  return lines.join('\n');
433
430
  }
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * v1 schema (intentionally tiny — extend via additive keys only):
8
8
  * {
9
- * "model": "my-proxy/claude-sonnet-4-20250514" // optional
9
+ * "model": "my-proxy/claude-sonnet-4-20250514", // optional
10
+ * "modelEffort": "high" // optional
10
11
  * }
11
12
  *
12
13
  * Missing file or `{}` → no session-level override. Missing field → fall back to user-level
@@ -26,7 +27,8 @@ import { sessionsRoot, resolveSessionYeaftDir } from './session-crud.js';
26
27
  const CONFIG_FILE = 'config.json';
27
28
 
28
29
  /** Whitelist of persisted session model-override fields. Reject everything else. */
29
- const ALLOWED_KEYS = new Set(['model']);
30
+ const ALLOWED_KEYS = new Set(['model', 'modelEffort']);
31
+ const ALLOWED_EFFORTS = new Set(['low', 'medium', 'high']);
30
32
 
31
33
  export class SessionConfigError extends Error {
32
34
  constructor(code, message) {
@@ -97,6 +99,11 @@ export function validateSessionConfig(cfg) {
97
99
  throw new SessionConfigError('invalid_model', 'model must be a non-empty string');
98
100
  }
99
101
  }
102
+ if ('modelEffort' in cfg && cfg.modelEffort !== null && cfg.modelEffort !== undefined && cfg.modelEffort !== '') {
103
+ if (typeof cfg.modelEffort !== 'string' || !ALLOWED_EFFORTS.has(cfg.modelEffort.trim())) {
104
+ throw new SessionConfigError('invalid_model_effort', 'modelEffort must be low, medium, or high');
105
+ }
106
+ }
100
107
  }
101
108
 
102
109
  /**
@@ -163,5 +170,10 @@ export function resolveSessionConfig(userConfig, sessionConfig) {
163
170
  base.model = model;
164
171
  base.primaryModel = model;
165
172
  }
173
+ if (overrides.modelEffort && typeof overrides.modelEffort === 'string' && ALLOWED_EFFORTS.has(overrides.modelEffort)) {
174
+ base.modelEffort = overrides.modelEffort;
175
+ } else {
176
+ delete base.modelEffort;
177
+ }
166
178
  return base;
167
179
  }
@@ -6,6 +6,6 @@ No VP soul is active for this turn. Participate in the current session with grou
6
6
 
7
7
  <!-- lang:zh -->
8
8
 
9
- # Session fallback identity
9
+ # Session 兜底身份
10
10
 
11
11
  当前回合没有激活 VP soul。你在当前 session 中参与协作,回答要基于证据,并保持用户上下文。
@@ -10,7 +10,7 @@ This legacy template is no longer injected for normal VP turns. Keep any future
10
10
 
11
11
  <!-- lang:zh -->
12
12
 
13
- # Session behavior notes
13
+ # Session 行为说明
14
14
 
15
15
  这个 legacy template 不再注入普通 VP turn。未来如果重新使用,也只能放行为规则:不要在这里定义身份;身份必须来自选中的 VP soul 或无 VP fallback。
16
16
 
@@ -2796,6 +2796,7 @@ async function ensureSessionLoaded() {
2796
2796
  type: 'session_ready',
2797
2797
  conversationId: yeaftConversationId,
2798
2798
  model: session.config.model,
2799
+ modelEffort: session.config.modelEffort || null,
2799
2800
  availableModels: session.config.availableModels || [],
2800
2801
  skills: session.status.skills,
2801
2802
  mcpServers: session.status.mcpServers,
@@ -3920,23 +3921,34 @@ export function handleYeaftModeSwitch(_msg) {
3920
3921
  }
3921
3922
 
3922
3923
 
3924
+
3925
+ export function modelRefMatchesAvailable(model, requested) {
3926
+ if (!model || !requested) return false;
3927
+ return model.id === requested
3928
+ || model.ref === requested
3929
+ || (model.provider && model.id && `${model.provider}/${model.id}` === requested);
3930
+ }
3931
+
3923
3932
  /** Handle model switch from the web UI. */
3924
3933
  export function handleYeaftModelSwitch(msg) {
3925
3934
  if (!session || !msg.model) return;
3926
3935
  refreshLiveSessionConfig();
3927
3936
 
3928
3937
  const available = session.config.availableModels || [];
3929
- const found = available.some(m => m.id === msg.model);
3938
+ const found = available.some(m => modelRefMatchesAvailable(m, msg.model));
3930
3939
  if (!found) {
3931
3940
  console.warn(`[Yeaft] model switch rejected — "${msg.model}" not in availableModels`);
3932
3941
  return;
3933
3942
  }
3934
3943
 
3935
3944
  session.config.model = msg.model;
3945
+ session.config.primaryModel = msg.model;
3946
+ session.config.modelEffort = msg.modelEffort || null;
3936
3947
 
3937
3948
  sendSessionEvent({
3938
3949
  type: 'model_switched',
3939
3950
  model: msg.model,
3951
+ modelEffort: session.config.modelEffort || null,
3940
3952
  });
3941
3953
  }
3942
3954
 
@@ -3996,6 +4008,7 @@ export async function handleYeaftLoadHistory(msg) {
3996
4008
  type: 'session_ready',
3997
4009
  conversationId: yeaftConversationId,
3998
4010
  model: session.config.model,
4011
+ modelEffort: session.config.modelEffort || null,
3999
4012
  availableModels: session.config.availableModels || [],
4000
4013
  skills: session.status.skills,
4001
4014
  mcpServers: session.status.mcpServers,