@yeaft/webchat-agent 1.0.189 → 1.0.191
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/connection/index.js +13 -6
- package/connection/message-router.js +23 -4
- package/local-runtime/server/handlers/agent-output.js +1 -0
- package/local-runtime/server/handlers/agent-sync.js +2 -0
- package/local-runtime/server/handlers/client-misc.js +2 -0
- package/local-runtime/server/ws-agent.js +20 -14
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +90 -90
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/config-api.js +57 -2
- package/yeaft/engine.js +16 -8
- package/yeaft/llm/router.js +16 -3
- package/yeaft/sessions/session-config.js +159 -76
- package/yeaft/status-cache.js +74 -17
- package/yeaft/web-bridge.js +99 -47
|
Binary file
|
package/package.json
CHANGED
package/yeaft/config-api.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
import { DEFAULT_YEAFT_DIR } from './init.js';
|
|
14
|
-
import { normalizeProviderModels, serializeModelForPersistence } from './models.js';
|
|
14
|
+
import { normalizeProviderModels, parseModelRef, serializeModelForPersistence } from './models.js';
|
|
15
15
|
import { normaliseYeaftSection } from './config.js';
|
|
16
16
|
import { isGitHubCopilotProvider, serializeKnownProviderForPersistence } from './llm/known-providers.js';
|
|
17
17
|
|
|
@@ -54,6 +54,58 @@ export function getLlmConfig(dir) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function providerModelIds(provider) {
|
|
58
|
+
return new Set(normalizeProviderModels(provider).map(model => model.id));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function bareModelOwners(providers, modelId) {
|
|
62
|
+
const owners = new Set();
|
|
63
|
+
for (const provider of providers) {
|
|
64
|
+
if (provider?.name && providerModelIds(provider).has(modelId)) owners.add(provider.name);
|
|
65
|
+
}
|
|
66
|
+
return owners;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function modelSelectionIsValid(selection, providers, authoritativeManagedNames) {
|
|
70
|
+
if (!selection) return false;
|
|
71
|
+
const parsed = parseModelRef(selection);
|
|
72
|
+
if (!parsed.modelId) return false;
|
|
73
|
+
if (!parsed.providerName) return bareModelOwners(providers, parsed.modelId).size === 1;
|
|
74
|
+
if (!authoritativeManagedNames.has(parsed.providerName)) return true;
|
|
75
|
+
const provider = providers.find(item => item?.name === parsed.providerName);
|
|
76
|
+
return !!provider && providerModelIds(provider).has(parsed.modelId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeManagedModelDefaults(config) {
|
|
80
|
+
const providers = Array.isArray(config.providers) ? config.providers : [];
|
|
81
|
+
const authoritativeManagedProviders = providers.filter(provider =>
|
|
82
|
+
isGitHubCopilotProvider(provider)
|
|
83
|
+
&& Array.isArray(provider.models)
|
|
84
|
+
&& provider.models.length > 0);
|
|
85
|
+
if (authoritativeManagedProviders.length === 0) return;
|
|
86
|
+
|
|
87
|
+
const authoritativeManagedNames = new Set(authoritativeManagedProviders.map(provider => provider.name));
|
|
88
|
+
const fallbackProvider = authoritativeManagedProviders.find(provider => providerModelIds(provider).size > 0);
|
|
89
|
+
const fallbackModelId = fallbackProvider
|
|
90
|
+
? providerModelIds(fallbackProvider).values().next().value
|
|
91
|
+
: null;
|
|
92
|
+
const managedFallback = fallbackProvider && fallbackModelId
|
|
93
|
+
? `${fallbackProvider.name}/${fallbackModelId}`
|
|
94
|
+
: null;
|
|
95
|
+
|
|
96
|
+
if (config.primaryModel
|
|
97
|
+
&& !modelSelectionIsValid(config.primaryModel, providers, authoritativeManagedNames)) {
|
|
98
|
+
config.primaryModel = managedFallback;
|
|
99
|
+
}
|
|
100
|
+
const validPrimary = modelSelectionIsValid(config.primaryModel, providers, authoritativeManagedNames)
|
|
101
|
+
? config.primaryModel
|
|
102
|
+
: managedFallback;
|
|
103
|
+
if (config.fastModel
|
|
104
|
+
&& !modelSelectionIsValid(config.fastModel, providers, authoritativeManagedNames)) {
|
|
105
|
+
config.fastModel = validPrimary;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
57
109
|
/**
|
|
58
110
|
* Update the LLM-relevant portion of config.json.
|
|
59
111
|
* Merges into existing config — preserves fields like debug, maxContextTokens, etc.
|
|
@@ -109,13 +161,16 @@ export function updateLlmConfig(update, dir) {
|
|
|
109
161
|
});
|
|
110
162
|
}
|
|
111
163
|
|
|
112
|
-
// Update model selections
|
|
164
|
+
// Update model selections. A managed provider catalog is authoritative:
|
|
165
|
+
// when it drops the old Agent default, keep no hidden reference to that
|
|
166
|
+
// model in primaryModel or fastModel.
|
|
113
167
|
if (update.primaryModel !== undefined) {
|
|
114
168
|
existing.primaryModel = update.primaryModel || null;
|
|
115
169
|
}
|
|
116
170
|
if (update.fastModel !== undefined) {
|
|
117
171
|
existing.fastModel = update.fastModel || null;
|
|
118
172
|
}
|
|
173
|
+
if (update.providers !== undefined) normalizeManagedModelDefaults(existing);
|
|
119
174
|
if (update.language !== undefined) {
|
|
120
175
|
existing.language = update.language;
|
|
121
176
|
}
|
package/yeaft/engine.js
CHANGED
|
@@ -683,14 +683,7 @@ export class Engine {
|
|
|
683
683
|
conversationId: this.#traceId,
|
|
684
684
|
});
|
|
685
685
|
|
|
686
|
-
|
|
687
|
-
// Falls back to primary model if no fastModel configured
|
|
688
|
-
const fastModelId = config.fastModelId || config.model;
|
|
689
|
-
if (fastModelId !== config.model) {
|
|
690
|
-
this.#fastConfig = { ...config, model: fastModelId };
|
|
691
|
-
} else {
|
|
692
|
-
this.#fastConfig = config;
|
|
693
|
-
}
|
|
686
|
+
this.refreshConfig(config);
|
|
694
687
|
}
|
|
695
688
|
|
|
696
689
|
/**
|
|
@@ -759,6 +752,21 @@ export class Engine {
|
|
|
759
752
|
this.#config.language = lang;
|
|
760
753
|
}
|
|
761
754
|
|
|
755
|
+
/**
|
|
756
|
+
* Replace the effective runtime config for subsequent queries without
|
|
757
|
+
* interrupting a turn that already captured its model.
|
|
758
|
+
*
|
|
759
|
+
* @param {object} config
|
|
760
|
+
*/
|
|
761
|
+
refreshConfig(config) {
|
|
762
|
+
if (!config || typeof config !== 'object') return;
|
|
763
|
+
this.#config = config;
|
|
764
|
+
const fastModelId = config.fastModelId || config.model;
|
|
765
|
+
this.#fastConfig = fastModelId !== config.model
|
|
766
|
+
? { ...config, model: fastModelId }
|
|
767
|
+
: config;
|
|
768
|
+
}
|
|
769
|
+
|
|
762
770
|
/**
|
|
763
771
|
* Hot-swap runtime managers for a project-bound Session. The ToolRegistry is
|
|
764
772
|
* shared at the bridge/session layer; these references only decide which
|
package/yeaft/llm/router.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
GITHUB_COPILOT_BASE_URL,
|
|
28
28
|
GITHUB_COPILOT_CREDENTIAL_PROVIDER,
|
|
29
29
|
GITHUB_COPILOT_PROVIDER_NAME,
|
|
30
|
+
isGitHubCopilotProvider,
|
|
30
31
|
normalizeKnownProviderForRuntime,
|
|
31
32
|
} from './known-providers.js';
|
|
32
33
|
import { pairSanitize } from '../pair-sanitize.js';
|
|
@@ -238,9 +239,12 @@ export class AdapterRouter extends LLMAdapter {
|
|
|
238
239
|
/** @type {Map<string, LLMAdapter>} providerName::protocol → cached adapter */
|
|
239
240
|
#adapterCache;
|
|
240
241
|
|
|
241
|
-
/** @type {object[]}
|
|
242
|
+
/** @type {object[]} normalized providers array */
|
|
242
243
|
#providers;
|
|
243
244
|
|
|
245
|
+
/** @type {Set<string>} managed providers backed by an explicit model catalog */
|
|
246
|
+
#authoritativeManagedProviders;
|
|
247
|
+
|
|
244
248
|
/** @type {number} per-SSE-chunk silence budget; <= 0 disables the guard */
|
|
245
249
|
#streamIdleTimeoutMs;
|
|
246
250
|
|
|
@@ -267,8 +271,13 @@ export class AdapterRouter extends LLMAdapter {
|
|
|
267
271
|
* @param {object[]} providers
|
|
268
272
|
*/
|
|
269
273
|
refreshProviders(providers) {
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
const rawProviders = Array.isArray(providers) ? providers : [];
|
|
275
|
+
this.#authoritativeManagedProviders = new Set(rawProviders
|
|
276
|
+
.filter(provider => isGitHubCopilotProvider(provider)
|
|
277
|
+
&& Array.isArray(provider.models)
|
|
278
|
+
&& provider.models.length > 0)
|
|
279
|
+
.map(provider => provider.name || GITHUB_COPILOT_PROVIDER_NAME));
|
|
280
|
+
this.#providers = rawProviders.map(provider => normalizeKnownProviderForRuntime(provider));
|
|
272
281
|
this.#modelToProvider = new Map();
|
|
273
282
|
this.#adapterCache = new Map();
|
|
274
283
|
|
|
@@ -306,6 +315,10 @@ export class AdapterRouter extends LLMAdapter {
|
|
|
306
315
|
|
|
307
316
|
const candidates = this.#providers.filter(p => p && p.name === parsed.providerName);
|
|
308
317
|
if (candidates.length === 0) return null;
|
|
318
|
+
// An explicit managed catalog is authoritative. A qualified ref is not
|
|
319
|
+
// permission to resurrect a model that catalog just removed. Legacy rows
|
|
320
|
+
// with no models still use the bundled fallback catalog above.
|
|
321
|
+
if (this.#authoritativeManagedProviders.has(parsed.providerName)) return null;
|
|
309
322
|
|
|
310
323
|
const inferred = inferProtocolFromModelId(parsed.modelId);
|
|
311
324
|
if (!inferred) return null;
|
|
@@ -7,30 +7,27 @@
|
|
|
7
7
|
* Session config is user-level state. Project `.yeaft` directories may provide
|
|
8
8
|
* project assets such as skills/MCP config, but must not own Session config.
|
|
9
9
|
*
|
|
10
|
-
* v1 schema
|
|
10
|
+
* Public v1 schema:
|
|
11
11
|
* {
|
|
12
12
|
* "model": "my-proxy/claude-sonnet-4-20250514", // optional
|
|
13
13
|
* "modelEffort": "high" // optional
|
|
14
14
|
* }
|
|
15
15
|
*
|
|
16
|
-
* Missing file or `{}`
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* Storage layer only — no engine wiring, no validation of model strings
|
|
21
|
-
* against the provider registry (that's done lazily at resolve time by
|
|
22
|
-
* the engine when it tries to dispatch to AdapterRouter).
|
|
16
|
+
* Missing file or `{}` means no Session-level override. Managed provider
|
|
17
|
+
* catalogs are authoritative: an override removed from such a catalog cannot
|
|
18
|
+
* reach the adapter and falls back to the current Agent default.
|
|
23
19
|
*/
|
|
24
20
|
|
|
25
21
|
import { existsSync, readFileSync } from 'fs';
|
|
26
22
|
import { join } from 'path';
|
|
27
23
|
import { writeAtomic } from '../storage/index.js';
|
|
24
|
+
import { isGitHubCopilotProvider } from '../llm/known-providers.js';
|
|
28
25
|
import { sessionsRoot } from './session-crud.js';
|
|
29
26
|
|
|
30
27
|
const CONFIG_FILE = 'config.json';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
28
|
+
const MODEL_SOURCE_EXPLICIT = 'explicit';
|
|
29
|
+
const WRITABLE_KEYS = new Set(['model', 'modelEffort']);
|
|
30
|
+
const STORED_KEYS = new Set([...WRITABLE_KEYS, 'modelSource']);
|
|
34
31
|
const ALLOWED_EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
35
32
|
|
|
36
33
|
export class SessionConfigError extends Error {
|
|
@@ -41,36 +38,30 @@ export class SessionConfigError extends Error {
|
|
|
41
38
|
}
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
/**
|
|
45
|
-
* Resolve the on-disk path for a session's config.json. Always uses the
|
|
46
|
-
* agent-local Yeaft root (`~/.yeaft` in production), not a project `.yeaft`.
|
|
47
|
-
*/
|
|
41
|
+
/** Resolve the agent-local path for a Session's config.json. */
|
|
48
42
|
export function sessionConfigPath(yeaftDir, sessionId) {
|
|
49
43
|
if (!yeaftDir) return null;
|
|
50
44
|
return join(sessionsRoot(yeaftDir), sessionId, CONFIG_FILE);
|
|
51
45
|
}
|
|
52
46
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
export function loadSessionConfig(yeaftDir, sessionId) {
|
|
47
|
+
function publicConfig(stored) {
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const key of WRITABLE_KEYS) {
|
|
50
|
+
if (Object.prototype.hasOwnProperty.call(stored || {}, key)) out[key] = stored[key];
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function loadStoredSessionConfig(yeaftDir, sessionId) {
|
|
63
56
|
if (!sessionId || !yeaftDir) return {};
|
|
64
57
|
const path = sessionConfigPath(yeaftDir, sessionId);
|
|
65
58
|
if (!path || !existsSync(path)) return {};
|
|
66
59
|
try {
|
|
67
60
|
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
68
61
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
|
69
|
-
// Strip unknown keys at read time too — defensive against
|
|
70
|
-
// hand-edited files that predate a key removal.
|
|
71
62
|
const out = {};
|
|
72
|
-
for (const
|
|
73
|
-
if (
|
|
63
|
+
for (const key of Object.keys(parsed)) {
|
|
64
|
+
if (STORED_KEYS.has(key)) out[key] = parsed[key];
|
|
74
65
|
}
|
|
75
66
|
return out;
|
|
76
67
|
} catch {
|
|
@@ -78,21 +69,27 @@ export function loadSessionConfig(yeaftDir, sessionId) {
|
|
|
78
69
|
}
|
|
79
70
|
}
|
|
80
71
|
|
|
72
|
+
function persistStoredSessionConfig(yeaftDir, sessionId, stored) {
|
|
73
|
+
const path = sessionConfigPath(yeaftDir, sessionId);
|
|
74
|
+
writeAtomic(path, `${JSON.stringify(stored, null, 2)}\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
81
77
|
/**
|
|
82
|
-
*
|
|
83
|
-
* any unknown key or malformed value. Empty / null values for known
|
|
84
|
-
* keys are allowed — they signal "fall back to user default".
|
|
85
|
-
*
|
|
86
|
-
* @param {object} cfg
|
|
78
|
+
* Read public Session config. Internal provenance never crosses this boundary.
|
|
87
79
|
*/
|
|
80
|
+
export function loadSessionConfig(yeaftDir, sessionId) {
|
|
81
|
+
return publicConfig(loadStoredSessionConfig(yeaftDir, sessionId));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Validate a user/wire supplied partial Session config. */
|
|
88
85
|
export function validateSessionConfig(cfg) {
|
|
89
86
|
if (cfg === null || cfg === undefined) return;
|
|
90
87
|
if (typeof cfg !== 'object' || Array.isArray(cfg)) {
|
|
91
88
|
throw new SessionConfigError('invalid_shape', 'config must be an object');
|
|
92
89
|
}
|
|
93
|
-
for (const
|
|
94
|
-
if (!
|
|
95
|
-
throw new SessionConfigError('unknown_key', `unknown config key: ${
|
|
90
|
+
for (const key of Object.keys(cfg)) {
|
|
91
|
+
if (!WRITABLE_KEYS.has(key)) {
|
|
92
|
+
throw new SessionConfigError('unknown_key', `unknown config key: ${key}`);
|
|
96
93
|
}
|
|
97
94
|
}
|
|
98
95
|
if ('model' in cfg && cfg.model !== null && cfg.model !== undefined && cfg.model !== '') {
|
|
@@ -108,68 +105,154 @@ export function validateSessionConfig(cfg) {
|
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* back to the user default).
|
|
115
|
-
*
|
|
116
|
-
* @param {string} yeaftDir
|
|
117
|
-
* @param {string} sessionId
|
|
118
|
-
* @param {object} partial
|
|
119
|
-
* @returns {object}
|
|
108
|
+
* Persist an explicit Session config change. `modelSource` is internal and is
|
|
109
|
+
* set here rather than accepted from clients, so future migrations can
|
|
110
|
+
* distinguish a user choice from the retired automatic default seed.
|
|
120
111
|
*/
|
|
121
112
|
export function saveSessionConfig(yeaftDir, sessionId, partial) {
|
|
122
113
|
if (!sessionId) throw new SessionConfigError('missing_group_id', 'sessionId required');
|
|
123
114
|
validateSessionConfig(partial);
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
delete next
|
|
130
|
-
|
|
131
|
-
next[k] = typeof v === 'string' ? v.trim() : v;
|
|
115
|
+
const next = { ...loadStoredSessionConfig(yeaftDir, sessionId) };
|
|
116
|
+
for (const [key, value] of Object.entries(partial || {})) {
|
|
117
|
+
if (!WRITABLE_KEYS.has(key)) continue;
|
|
118
|
+
if (value === null || value === undefined || value === '') {
|
|
119
|
+
delete next[key];
|
|
120
|
+
if (key === 'model') delete next.modelSource;
|
|
121
|
+
continue;
|
|
132
122
|
}
|
|
123
|
+
next[key] = typeof value === 'string' ? value.trim() : value;
|
|
124
|
+
if (key === 'model') next.modelSource = MODEL_SOURCE_EXPLICIT;
|
|
133
125
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return next;
|
|
126
|
+
persistStoredSessionConfig(yeaftDir, sessionId, next);
|
|
127
|
+
return publicConfig(next);
|
|
137
128
|
}
|
|
138
129
|
|
|
139
|
-
/**
|
|
140
|
-
* Initialise an empty config.json next to a brand-new group. Idempotent —
|
|
141
|
-
* leaves an existing file untouched. Called by `createSessionFromSpec`.
|
|
142
|
-
*/
|
|
130
|
+
/** Create an empty Session config file without inventing an override. */
|
|
143
131
|
export function ensureSessionConfigFile(yeaftDir, sessionId) {
|
|
144
132
|
const path = sessionConfigPath(yeaftDir, sessionId);
|
|
145
133
|
if (!path || existsSync(path)) return;
|
|
146
134
|
try {
|
|
147
|
-
|
|
135
|
+
persistStoredSessionConfig(yeaftDir, sessionId, {});
|
|
148
136
|
} catch {
|
|
149
|
-
// Best-effort — a permission failure
|
|
150
|
-
// create. Read path returns {} on missing file anyway.
|
|
137
|
+
// Best-effort — a permission failure must not break Session creation.
|
|
151
138
|
}
|
|
152
139
|
}
|
|
153
140
|
|
|
141
|
+
function modelRefIdentity(value) {
|
|
142
|
+
const text = String(value || '');
|
|
143
|
+
const slash = text.indexOf('/');
|
|
144
|
+
return slash < 0
|
|
145
|
+
? { provider: '', modelId: text }
|
|
146
|
+
: { provider: text.slice(0, slash), modelId: text.slice(slash + 1) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function modelRefsEquivalent(left, right) {
|
|
150
|
+
if (!left || !right) return false;
|
|
151
|
+
if (left === right) return true;
|
|
152
|
+
const a = modelRefIdentity(left);
|
|
153
|
+
const b = modelRefIdentity(right);
|
|
154
|
+
if (a.modelId !== b.modelId) return false;
|
|
155
|
+
return !a.provider || !b.provider || a.provider === b.provider;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function hasAuthoritativeCatalog(provider) {
|
|
159
|
+
if (!provider || typeof provider !== 'object') return false;
|
|
160
|
+
if (provider.managed === true) return true;
|
|
161
|
+
if (typeof provider.managed === 'string' && provider.managed.trim()) return true;
|
|
162
|
+
return isGitHubCopilotProvider(provider);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function resolveAllowedModelRef(config, modelRef) {
|
|
166
|
+
const identity = modelRefIdentity(modelRef);
|
|
167
|
+
const providers = Array.isArray(config?.providers) ? config.providers : [];
|
|
168
|
+
const availableModels = Array.isArray(config?.availableModels) ? config.availableModels : [];
|
|
169
|
+
|
|
170
|
+
if (identity.provider) {
|
|
171
|
+
const provider = providers.find(item => item?.name === identity.provider);
|
|
172
|
+
if (!hasAuthoritativeCatalog(provider)) return modelRef;
|
|
173
|
+
const match = availableModels.find(model => model?.ref === modelRef
|
|
174
|
+
|| (model?.provider === identity.provider && model?.id === identity.modelId));
|
|
175
|
+
return match?.ref || (match ? `${identity.provider}/${identity.modelId}` : null);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const authoritativeProviderNames = new Set(providers
|
|
179
|
+
.filter(hasAuthoritativeCatalog)
|
|
180
|
+
.map(provider => provider.name)
|
|
181
|
+
.filter(Boolean));
|
|
182
|
+
if (authoritativeProviderNames.size === 0) return modelRef;
|
|
183
|
+
|
|
184
|
+
const candidates = new Map();
|
|
185
|
+
for (const model of availableModels) {
|
|
186
|
+
const candidateIdentity = modelRefIdentity(model?.ref || '');
|
|
187
|
+
const candidateId = model?.id || candidateIdentity.modelId;
|
|
188
|
+
const providerName = model?.provider || candidateIdentity.provider;
|
|
189
|
+
if (candidateId !== identity.modelId || !providerName) continue;
|
|
190
|
+
candidates.set(model?.ref || `${providerName}/${candidateId}`, providerName);
|
|
191
|
+
}
|
|
192
|
+
if (candidates.size !== 1) return null;
|
|
193
|
+
const [[ref]] = candidates;
|
|
194
|
+
return ref;
|
|
195
|
+
}
|
|
196
|
+
|
|
154
197
|
/**
|
|
155
|
-
*
|
|
156
|
-
* overrides on top of the user-level config.
|
|
157
|
-
*
|
|
158
|
-
* Only fields that the per-group schema knows about are overlaid;
|
|
159
|
-
* everything else (providers, language, token budgets, ...) is taken
|
|
160
|
-
* verbatim from the user config.
|
|
198
|
+
* Normalize persisted config at the Agent/Session boundary.
|
|
161
199
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
200
|
+
* - Managed catalog misses are always removed: they are no longer valid.
|
|
201
|
+
* - Legacy rows have no source bit. An unmarked value equal to the Agent's
|
|
202
|
+
* previous default is the retired automatic seed and becomes inheritance.
|
|
203
|
+
* - Values written through the explicit update path carry `modelSource` and
|
|
204
|
+
* are preserved while they remain in the authoritative catalog.
|
|
165
205
|
*/
|
|
206
|
+
export function normalizeSessionConfig(
|
|
207
|
+
yeaftDir,
|
|
208
|
+
sessionId,
|
|
209
|
+
userConfig,
|
|
210
|
+
{ previousDefaultModel = null } = {},
|
|
211
|
+
) {
|
|
212
|
+
const stored = loadStoredSessionConfig(yeaftDir, sessionId);
|
|
213
|
+
if (!stored.model || typeof stored.model !== 'string') return publicConfig(stored);
|
|
214
|
+
|
|
215
|
+
const allowedModel = resolveAllowedModelRef(userConfig, stored.model);
|
|
216
|
+
// The retired create path wrote the then-current Agent default without a
|
|
217
|
+
// source bit. New explicit writes always carry modelSource, so this is the
|
|
218
|
+
// only deterministic compatibility rule available for legacy rows.
|
|
219
|
+
const legacyAutoSeed = stored.modelSource !== MODEL_SOURCE_EXPLICIT
|
|
220
|
+
&& modelRefsEquivalent(stored.model, previousDefaultModel);
|
|
221
|
+
if (allowedModel && !legacyAutoSeed) {
|
|
222
|
+
const next = { ...stored };
|
|
223
|
+
let changed = false;
|
|
224
|
+
if (next.model !== allowedModel) {
|
|
225
|
+
next.model = allowedModel;
|
|
226
|
+
changed = true;
|
|
227
|
+
}
|
|
228
|
+
// Resolve every legacy row once while the old Agent default is known. A
|
|
229
|
+
// different value could only have come from an explicit Session choice;
|
|
230
|
+
// backfill provenance so a later default change cannot misclassify it.
|
|
231
|
+
if (previousDefaultModel && next.modelSource !== MODEL_SOURCE_EXPLICIT) {
|
|
232
|
+
next.modelSource = MODEL_SOURCE_EXPLICIT;
|
|
233
|
+
changed = true;
|
|
234
|
+
}
|
|
235
|
+
if (changed) persistStoredSessionConfig(yeaftDir, sessionId, next);
|
|
236
|
+
return publicConfig(next);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const next = { ...stored };
|
|
240
|
+
delete next.model;
|
|
241
|
+
delete next.modelSource;
|
|
242
|
+
persistStoredSessionConfig(yeaftDir, sessionId, next);
|
|
243
|
+
return publicConfig(next);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Overlay a valid Session override on the Agent config. */
|
|
166
247
|
export function resolveSessionConfig(userConfig, sessionConfig) {
|
|
167
248
|
const base = userConfig ? { ...userConfig } : {};
|
|
168
249
|
const overrides = sessionConfig && typeof sessionConfig === 'object' ? sessionConfig : {};
|
|
169
250
|
if (overrides.model && typeof overrides.model === 'string' && overrides.model.trim()) {
|
|
170
|
-
const model = overrides.model.trim();
|
|
171
|
-
|
|
172
|
-
|
|
251
|
+
const model = resolveAllowedModelRef(base, overrides.model.trim());
|
|
252
|
+
if (model) {
|
|
253
|
+
base.model = model;
|
|
254
|
+
base.primaryModel = model;
|
|
255
|
+
}
|
|
173
256
|
}
|
|
174
257
|
if (overrides.modelEffort && typeof overrides.modelEffort === 'string' && ALLOWED_EFFORTS.has(overrides.modelEffort)) {
|
|
175
258
|
base.modelEffort = overrides.modelEffort;
|