@yeaft/webchat-agent 0.1.833 → 0.1.836

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.
@@ -36,6 +36,7 @@ import { sendToServer, flushMessageBuffer } from './buffer.js';
36
36
  import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
37
37
  import { loadMcpServers, updateMcpConfig } from '../mcp.js';
38
38
  import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../unify/config-api.js';
39
+ import { fetchModelsDev } from '../unify/llm/models-dev.js';
39
40
  import { handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyLoadMoreHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyAbortTurn, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyUpdateGroup, handleUnifyUpdateGroupConfig, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger, handleUnifyFetchToolStats, handleUnifyFetchDebugHistory, broadcastLanguageChange } from '../unify/web-bridge.js';
40
41
 
41
42
  export async function handleMessage(msg) {
@@ -347,6 +348,31 @@ export async function handleMessage(msg) {
347
348
  break;
348
349
  }
349
350
 
351
+ // models.dev registry (community-maintained provider/model catalog).
352
+ // Used by the LLM settings preset picker to populate provider + model lists.
353
+ case 'get_models_dev_registry': {
354
+ try {
355
+ const data = await fetchModelsDev({
356
+ forceRefresh: !!msg.forceRefresh,
357
+ yeaftDir: ctx.CONFIG?.yeaftDir,
358
+ });
359
+ sendToServer({
360
+ type: 'models_dev_registry',
361
+ requestId: msg.requestId || null,
362
+ registry: data,
363
+ fetchedAt: Date.now(),
364
+ });
365
+ } catch (err) {
366
+ sendToServer({
367
+ type: 'models_dev_registry',
368
+ requestId: msg.requestId || null,
369
+ registry: {},
370
+ error: err?.message || String(err),
371
+ });
372
+ }
373
+ break;
374
+ }
375
+
350
376
  // task-318: Unify runtime settings (thread concurrency + auto-archive).
351
377
  // Read/write the nested `unify` section of config.json — LLM fields
352
378
  // untouched. On update we broadcast a `unify_settings_updated` event
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.833",
3
+ "version": "0.1.836",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,160 @@
1
+ /**
2
+ * models.dev registry integration — community-maintained provider/model database.
3
+ *
4
+ * Fetches https://models.dev/api.json (4000+ models across 109+ providers) and
5
+ * exposes provider/model metadata to the rest of the agent. Ported from hermes
6
+ * `agent/models_dev.py`.
7
+ *
8
+ * Cache hierarchy (when forceRefresh=false):
9
+ * 1. In-memory cache, populated and < TTL old → return immediately.
10
+ * 2. Disk cache file < TTL old by mtime → load, populate in-mem, return.
11
+ * 3. Network fetch → on success, save to disk + in-mem and return.
12
+ * 4. Network fails → fall back to ANY available disk cache (even stale)
13
+ * with a short 5 min in-mem grace period.
14
+ *
15
+ * forceRefresh=true skips stages 1 and 2 (used by manual refresh button).
16
+ */
17
+
18
+ import { readFile, writeFile, stat, mkdir, rename } from 'fs/promises';
19
+ import { join, dirname } from 'path';
20
+ import { homedir } from 'os';
21
+
22
+ const MODELS_DEV_URL = 'https://models.dev/api.json';
23
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
24
+ const NETWORK_TIMEOUT_MS = 15000;
25
+ const STALE_GRACE_MS = 5 * 60 * 1000; // 5 minutes when serving stale-after-failure
26
+
27
+ let _memCache = null;
28
+ let _memCacheTime = 0;
29
+
30
+ function getCachePath(yeaftDir) {
31
+ const base = yeaftDir || join(homedir(), '.yeaft');
32
+ return join(base, 'models_dev_cache.json');
33
+ }
34
+
35
+ async function diskCacheAgeMs(yeaftDir) {
36
+ try {
37
+ const s = await stat(getCachePath(yeaftDir));
38
+ const age = Date.now() - s.mtimeMs;
39
+ return age < 0 ? null : age;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ async function loadDiskCache(yeaftDir) {
46
+ try {
47
+ const raw = await readFile(getCachePath(yeaftDir), 'utf8');
48
+ return JSON.parse(raw);
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ async function saveDiskCache(yeaftDir, data) {
55
+ try {
56
+ const path = getCachePath(yeaftDir);
57
+ await mkdir(dirname(path), { recursive: true });
58
+ const tmp = path + '.tmp';
59
+ await writeFile(tmp, JSON.stringify(data), 'utf8');
60
+ await rename(tmp, path);
61
+ } catch (e) {
62
+ // Cache is best-effort. Failure to persist must not break callers.
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Fetch the models.dev registry with layered caching.
68
+ * @param {object} [opts]
69
+ * @param {boolean} [opts.forceRefresh] Skip in-mem + fresh-disk stages.
70
+ * @param {string} [opts.yeaftDir] Override ~/.yeaft cache directory.
71
+ * @returns {Promise<object>} provider-id → provider entry; {} on total failure.
72
+ */
73
+ export async function fetchModelsDev({ forceRefresh = false, yeaftDir = null } = {}) {
74
+ // Stage 1: in-memory cache.
75
+ if (!forceRefresh && _memCache && Date.now() - _memCacheTime < CACHE_TTL_MS) {
76
+ return _memCache;
77
+ }
78
+
79
+ // Stage 2: fresh-by-mtime disk cache short-circuits the network.
80
+ if (!forceRefresh) {
81
+ const age = await diskCacheAgeMs(yeaftDir);
82
+ if (age !== null && age < CACHE_TTL_MS) {
83
+ const data = await loadDiskCache(yeaftDir);
84
+ if (data && typeof data === 'object') {
85
+ _memCache = data;
86
+ _memCacheTime = Date.now() - age;
87
+ return _memCache;
88
+ }
89
+ }
90
+ }
91
+
92
+ // Stage 3: network fetch.
93
+ try {
94
+ const ctrl = new AbortController();
95
+ const timer = setTimeout(() => ctrl.abort(), NETWORK_TIMEOUT_MS);
96
+ try {
97
+ const res = await fetch(MODELS_DEV_URL, { signal: ctrl.signal });
98
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
99
+ const data = await res.json();
100
+ if (data && typeof data === 'object') {
101
+ _memCache = data;
102
+ _memCacheTime = Date.now();
103
+ await saveDiskCache(yeaftDir, data);
104
+ return data;
105
+ }
106
+ } finally {
107
+ clearTimeout(timer);
108
+ }
109
+ } catch {
110
+ // Fall through to stale cache.
111
+ }
112
+
113
+ // Stage 4: stale disk cache fallback with short grace TTL so we retry soon.
114
+ if (!_memCache) {
115
+ const data = await loadDiskCache(yeaftDir);
116
+ if (data && typeof data === 'object') {
117
+ _memCache = data;
118
+ _memCacheTime = Date.now() - CACHE_TTL_MS + STALE_GRACE_MS;
119
+ }
120
+ }
121
+
122
+ return _memCache || {};
123
+ }
124
+
125
+ /**
126
+ * List all provider ids in the registry.
127
+ * @returns {Promise<string[]>}
128
+ */
129
+ export async function listProviders({ yeaftDir = null } = {}) {
130
+ const data = await fetchModelsDev({ yeaftDir });
131
+ return Object.keys(data).sort();
132
+ }
133
+
134
+ /**
135
+ * Return raw provider entry from models.dev (with name, env, api, models).
136
+ */
137
+ export async function getProviderInfo(providerId, { yeaftDir = null } = {}) {
138
+ const data = await fetchModelsDev({ yeaftDir });
139
+ const entry = data[providerId];
140
+ return entry && typeof entry === 'object' ? entry : null;
141
+ }
142
+
143
+ /**
144
+ * List model ids advertised by a provider.
145
+ * @returns {Promise<string[]>}
146
+ */
147
+ export async function listProviderModels(providerId, { yeaftDir = null } = {}) {
148
+ const info = await getProviderInfo(providerId, { yeaftDir });
149
+ const models = info?.models;
150
+ if (!models || typeof models !== 'object') return [];
151
+ return Object.keys(models);
152
+ }
153
+
154
+ /**
155
+ * Reset the in-memory cache. Test seam.
156
+ */
157
+ export function _resetMemCache() {
158
+ _memCache = null;
159
+ _memCacheTime = 0;
160
+ }
package/unify/session.js CHANGED
@@ -193,11 +193,28 @@ export async function loadSession(options = {}) {
193
193
  console.warn(`[Yeaft] ${yeaftDir} is not writable — running in read-only mode`);
194
194
  }
195
195
 
196
- // ─── 3. Create debug trace ─────────────────────────────
196
+ // ─── 3. Create trajectory trace ─────────────────────────
197
+ // feat-always-on-trajectory-store: the trace is no longer gated on
198
+ // config.debug. It is a TRAJECTORY STORE: every turn's full
199
+ // (system_prompt, messages, tool_calls, tool_results, response, usage)
200
+ // is persisted to ~/.yeaft/debug.db so it can serve two purposes:
201
+ // 1. Debug panel hydration — user opens "请求日志" and sees prior turns.
202
+ // 2. SFT / RL training data — scripts can later dump JSONL trajectories.
203
+ // Cost is negligible (one insert per turn, WAL mode), and the data only
204
+ // accumulates while the user actually uses the agent. The previous gate
205
+ // silently discarded every turn unless the user had set debug:true in
206
+ // ~/.yeaft/config.json, which nobody ever did — wasting the asset.
197
207
  const trace = createTrace({
198
- enabled: config.debug,
208
+ enabled: true,
199
209
  dbPath: join(yeaftDir, 'debug.db'),
200
210
  });
211
+ // Bound disk growth: prune trajectories older than 30 days on session load.
212
+ // Cheap (indexed DELETE), runs once per process start, not per turn. Without
213
+ // this the always-on store grows unbounded — cleanup() existed but had zero
214
+ // call sites before this PR.
215
+ try { trace.cleanup?.(30); } catch (err) {
216
+ console.warn('[Yeaft] trace.cleanup failed:', err?.message || err);
217
+ }
201
218
 
202
219
  // ─── 4. Create LLM adapter ────────────────────────────
203
220
  const adapter = await createLLMAdapter(config);