@yeaft/webchat-agent 1.0.190 → 1.0.192

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.
@@ -53,7 +53,7 @@ import {
53
53
  resolveSessionYeaftDir,
54
54
  } from './sessions/session-crud.js';
55
55
  import { openSession, loadSessionMeta } from './sessions/session-store.js';
56
- import { loadSessionConfig, resolveSessionConfig, SessionConfigError } from './sessions/session-config.js';
56
+ import { loadSessionConfig, normalizeSessionConfig, resolveSessionConfig, SessionConfigError } from './sessions/session-config.js';
57
57
  import { updateSessionConfig } from './sessions/session-crud.js';
58
58
  import { createCoordinator } from './sessions/coordinator.js';
59
59
  import { seedDefaultSession } from './sessions/seed-default.js';
@@ -131,49 +131,93 @@ function applyLiveLanguage(language) {
131
131
  try { session?.engine?.setLanguage?.(language); } catch { /* best-effort */ }
132
132
  }
133
133
 
134
- function refreshLiveSessionConfig() {
135
- if (!session) return;
136
- try {
137
- const freshConfig = loadConfig({ dir: liveConfigRoot() });
138
- const freshModels = Array.isArray(freshConfig.availableModels) ? freshConfig.availableModels : [];
139
- session.config.availableModels = freshModels;
140
- if (freshConfig.language) {
141
- applyLiveLanguage(freshConfig.language);
142
- }
143
- if (freshConfig.model && !freshModels.some(m => modelRefMatchesAvailable(m, session.config.model))) {
144
- session.config.model = freshConfig.primaryModel || freshConfig.model;
145
- }
146
- if (freshConfig.providers) {
147
- session.config.providers = freshConfig.providers;
148
- if (typeof session.adapter?.refreshProviders === 'function') {
149
- session.adapter.refreshProviders(freshConfig.providers);
150
- }
151
- }
152
- } catch (err) {
153
- console.warn('[Yeaft] refresh live session config failed:', err?.message || err);
134
+ function modelRefIdentity(value) {
135
+ const text = String(value || '');
136
+ const slash = text.indexOf('/');
137
+ return slash < 0
138
+ ? { provider: '', modelId: text }
139
+ : { provider: text.slice(0, slash), modelId: text.slice(slash + 1) };
140
+ }
141
+
142
+ function modelRefsEquivalent(left, right) {
143
+ if (!left || !right) return false;
144
+ if (left === right) return true;
145
+ const a = modelRefIdentity(left);
146
+ const b = modelRefIdentity(right);
147
+ if (a.modelId !== b.modelId) return false;
148
+ return !a.provider || !b.provider || a.provider === b.provider;
149
+ }
150
+
151
+ function resolveLiveSessionConfig(baseConfig, sessionId, options = {}) {
152
+ const configRoot = baseConfig?.dir || liveConfigRoot();
153
+ const sessionConfig = normalizeSessionConfig(configRoot, sessionId, baseConfig, options);
154
+ return resolveSessionConfig(baseConfig, sessionConfig);
155
+ }
156
+
157
+ let sessionConfigRefreshRevision = 0;
158
+
159
+ /** Reload the Agent-owned config and install it into every live Engine. */
160
+ export async function refreshLiveSessionConfig(options = {}) {
161
+ sessionConfigRefreshRevision += 1;
162
+ if (!session && sessionLoadPromise) {
163
+ // The loader may run its own catch-up refresh, but it does not know this
164
+ // config transaction's pre-save default. Wait for it, then continue once
165
+ // with the original normalization input.
166
+ await sessionLoadPromise;
167
+ }
168
+
169
+ const configRoot = liveConfigRoot();
170
+ const freshConfig = loadConfig({ dir: configRoot });
171
+ const previousDefaultModel = options.previousDefaultModel
172
+ || session?.config?.primaryModel
173
+ || session?.config?.model
174
+ || null;
175
+ // Normalize disk state even before the runtime is loaded. The config-save
176
+ // transaction supplies the pre-write default so legacy automatic seeds can
177
+ // become inheritance without depending on page/session lifecycle.
178
+ for (const row of snapshotSessions(configRoot)) {
179
+ normalizeSessionConfig(configRoot, row.id, freshConfig, { previousDefaultModel });
180
+ }
181
+ if (!session) return freshConfig;
182
+
183
+ const liveSession = session;
184
+ const currentConfig = liveSession.config || {};
185
+ const runtimeOnly = {};
186
+ for (const key of ['serverMode', '_readOnly', 'modelEffort']) {
187
+ if (Object.prototype.hasOwnProperty.call(currentConfig, key)) runtimeOnly[key] = currentConfig[key];
188
+ }
189
+ const inheritedAgentDefault = !currentConfig.model
190
+ || modelRefsEquivalent(currentConfig.model, currentConfig.primaryModel);
191
+ const nextModel = inheritedAgentDefault
192
+ ? (freshConfig.primaryModel || freshConfig.model)
193
+ : currentConfig.model;
194
+ const nextConfig = { ...freshConfig, ...runtimeOnly, model: nextModel };
195
+ const vpConfigSnapshots = [];
196
+ for (const [key, engine] of vpEngines) {
197
+ const separator = key.indexOf('::');
198
+ if (separator < 1) continue;
199
+ const sessionId = key.slice(0, separator);
200
+ vpConfigSnapshots.push({
201
+ key,
202
+ engine,
203
+ config: resolveLiveSessionConfig(nextConfig, sessionId, { previousDefaultModel }),
204
+ });
154
205
  }
155
- }
156
206
 
157
- function defaultSessionModelConfig(baseConfig) {
158
- const config = baseConfig || session?.config || {};
159
- const out = {};
160
- const model = typeof config.primaryModel === 'string' && config.primaryModel.trim()
161
- ? config.primaryModel.trim()
162
- : (typeof config.model === 'string' && config.model.trim() ? config.model.trim() : '');
163
- if (model) out.model = model;
164
- if (typeof config.modelEffort === 'string' && config.modelEffort.trim()) {
165
- out.modelEffort = config.modelEffort.trim();
207
+ if (typeof liveSession.adapter?.refreshProviders === 'function') {
208
+ liveSession.adapter.refreshProviders(freshConfig.providers || []);
166
209
  }
167
- return out;
210
+ for (const key of Object.keys(currentConfig)) delete currentConfig[key];
211
+ Object.assign(currentConfig, nextConfig);
212
+ liveSession.engine?.refreshConfig?.(currentConfig);
213
+ for (const { key, engine, config } of vpConfigSnapshots) {
214
+ engine.refreshConfig?.(config);
215
+ vpEngineConfigKeys.set(key, engineConfigKey(config));
216
+ }
217
+ if (freshConfig.language) applyLiveLanguage(freshConfig.language);
218
+ return currentConfig;
168
219
  }
169
220
 
170
- function withDefaultSessionConfig(payload) {
171
- const next = payload && typeof payload === 'object' ? { ...payload } : {};
172
- const existing = next.config && typeof next.config === 'object' ? next.config : {};
173
- const seeded = { ...defaultSessionModelConfig(), ...existing };
174
- if (Object.keys(seeded).length > 0) next.config = seeded;
175
- return next;
176
- }
177
221
 
178
222
  /** Test-only: replace the lightweight VP thread classifier. */
179
223
  export function __testSetThreadClassifier(fn) {
@@ -210,7 +254,11 @@ function scheduleYeaftLoadHistoryMetadataReplay(sessionId) {
210
254
  setTimeout(async () => {
211
255
  try {
212
256
  if (!replaySession) return;
213
- refreshLiveSessionConfig();
257
+ try {
258
+ await refreshLiveSessionConfig();
259
+ } catch (err) {
260
+ console.warn('[Yeaft] load-history config refresh failed:', err?.message || err);
261
+ }
214
262
  let projectRuntime = null;
215
263
  if (sessionId) {
216
264
  try {
@@ -517,6 +565,7 @@ function engineConfigKey(config) {
517
565
  fastModel: config?.fastModel || '',
518
566
  fastModelId: config?.fastModelId || '',
519
567
  fallbackModel: config?.fallbackModel || '',
568
+ providers: Array.isArray(config?.providers) ? config.providers : [],
520
569
  });
521
570
  }
522
571
 
@@ -833,7 +882,7 @@ function queryTimeoutMsForSessionConfig(config = null) {
833
882
  function queryTimeoutMsForSession(sessionId = null) {
834
883
  if (!sessionId || !session) return queryTimeoutMsForSessionConfig(session?.config);
835
884
  try {
836
- return queryTimeoutMsForSessionConfig(resolveSessionConfig(session.config, loadSessionConfig(liveConfigRoot(), sessionId)));
885
+ return queryTimeoutMsForSessionConfig(resolveLiveSessionConfig(session.config, sessionId));
837
886
  } catch {
838
887
  return queryTimeoutMsForSessionConfig(session?.config);
839
888
  }
@@ -1337,8 +1386,7 @@ export function __testGroupHistory(sessionId) {
1337
1386
 
1338
1387
  export function __testResolveVpEffectiveConfig(sessionId) {
1339
1388
  if (!session) return null;
1340
- const sessionConfigRoot = liveConfigRoot();
1341
- return resolveSessionConfig(session.config, loadSessionConfig(sessionConfigRoot, sessionId));
1389
+ return resolveLiveSessionConfig(session.config, sessionId);
1342
1390
  }
1343
1391
 
1344
1392
  /**
@@ -1466,9 +1514,7 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
1466
1514
  // Per-session config overlay (v1: model only). Falls back to the
1467
1515
  // session's user-level config when no override is set. Session config is
1468
1516
  // always resolved from the agent-local root; project `.yeaft` is ignored.
1469
- const sessionConfigRoot = liveConfigRoot();
1470
- const groupCfg = loadSessionConfig(sessionConfigRoot, sessionId);
1471
- const effectiveConfig = resolveSessionConfig(session.config, groupCfg);
1517
+ const effectiveConfig = resolveLiveSessionConfig(session.config, sessionId);
1472
1518
  const configKey = engineConfigKey(effectiveConfig);
1473
1519
  let eng = vpEngines.get(key);
1474
1520
  if (eng && vpEngineConfigKeys.get(key) === configKey) return eng;
@@ -2684,7 +2730,7 @@ export function handleYeaftCreateSession(msg) {
2684
2730
  const payload = (msg && msg.payload) || {};
2685
2731
  try {
2686
2732
  const yeaftDir = ctx.CONFIG?.yeaftDir;
2687
- const group = createSessionFromSpec(yeaftDir, withDefaultSessionConfig(payload));
2733
+ const group = createSessionFromSpec(yeaftDir, payload);
2688
2734
  recordAgentSessionCreated();
2689
2735
  group.config = loadSessionConfig(yeaftDir, group.id);
2690
2736
  sendSessionCrudResult({ op: 'create', requestId, ok: true, session: group });
@@ -4212,6 +4258,7 @@ export async function ensureSessionLoaded(opts = {}) {
4212
4258
  if (sessionLoadPromise) return sessionLoadPromise;
4213
4259
 
4214
4260
  sessionLoadPromise = (async () => {
4261
+ const bootConfigRevision = sessionConfigRefreshRevision;
4215
4262
  const yeaftDir = ctx.CONFIG?.yeaftDir;
4216
4263
  const normalizedWorkDir = normalizeSessionWorkDir(opts?.workDir || opts?.sessionMeta?.workDir);
4217
4264
  session = await loadSession({
@@ -4223,6 +4270,11 @@ export async function ensureSessionLoaded(opts = {}) {
4223
4270
  });
4224
4271
 
4225
4272
  installYeaftRuntimeBridge(session);
4273
+ // A save may complete after loadSession read config.json but before this
4274
+ // runtime became visible. Refresh before any status/session_ready emit.
4275
+ if (sessionConfigRefreshRevision !== bootConfigRevision) {
4276
+ await refreshLiveSessionConfig();
4277
+ }
4226
4278
 
4227
4279
  try {
4228
4280
  if (session.engine && typeof session.engine.setSubAgentEventSink === 'function') {
@@ -5,7 +5,7 @@ import {
5
5
  sanitizeDebugValue,
6
6
  sanitizeDiagnosticText,
7
7
  } from './debug-projection.js';
8
- import { normalizeActionBrief } from './workflow.js';
8
+ import { taskSpecificActionBrief } from './workflow.js';
9
9
 
10
10
  const MAX_ACTION_MESSAGE_CHARS = 16_000;
11
11
  const MAX_ACTION_DIAGNOSTIC_CHARS = 8_000;
@@ -277,7 +277,7 @@ function projectAction(action, runs, events, includeBody = true) {
277
277
  if (!action) return null;
278
278
  const execution = actionExecution(action, runs, events, includeBody);
279
279
  const alreadyProjected = !Array.isArray(runs) && Array.isArray(action.messages);
280
- const brief = alreadyProjected ? (action.brief || null) : normalizeActionBrief(action.brief, action.type);
280
+ const brief = taskSpecificActionBrief(action.brief, action.type);
281
281
  const projectedBrief = includeBody || !brief
282
282
  ? brief
283
283
  : Object.fromEntries(Object.entries(brief).map(([key, value]) => [
@@ -301,7 +301,7 @@ export function createSubmitWorkItemPlanTool({ vps, collector, isRunActive }) {
301
301
  const catalogDescription = `Action types: ${actionTypes.join(', ')}. Available VPs: ${vpCatalog.map(vp => `${vp.id} (${vp.role || vp.area || 'VP'}; ${vp.traits.join(', ') || 'no traits'})`).join('; ')}.`;
302
302
  return defineTool({
303
303
  name: 'SubmitWorkItemPlan',
304
- description: `Submit the complete initial WorkItem contract and executable Action DAG. This tool records a Run-local proposal only; Work Center validates and persists it in the current Run finalization transaction. ${catalogDescription}`,
304
+ description: `Submit the complete initial WorkItem contract and executable Action DAG. Every Action must describe this WorkItem's concrete objective, repository-aware approach, and verifiable expected outcome; never copy generic Action-type text. The reference workflow catalog does not replace this Action list. This tool records a Run-local proposal only; Work Center validates and persists it in the current Run finalization transaction. ${catalogDescription}`,
305
305
  parameters: {
306
306
  type: 'object',
307
307
  additionalProperties: false,
@@ -1028,7 +1028,7 @@ export class WorkItemRunner {
1028
1028
  }
1029
1029
  return {
1030
1030
  ...parsedResult,
1031
- response,
1031
+ response: response || parsedResult.summary,
1032
1032
  ...executionStats(),
1033
1033
  checkpoint,
1034
1034
  };
@@ -62,6 +62,13 @@ function defaultActionBrief(type) {
62
62
  return { objective, approach, expectedOutcome };
63
63
  }
64
64
 
65
+ function hasCompleteActionBrief(value) {
66
+ return value && typeof value === 'object' && !Array.isArray(value)
67
+ && ['objective', 'approach', 'expectedOutcome'].every(key => (
68
+ typeof value[key] === 'string' && value[key].trim()
69
+ ));
70
+ }
71
+
65
72
  export function normalizeActionBrief(value, type) {
66
73
  const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
67
74
  const defaults = defaultActionBrief(type);
@@ -73,6 +80,13 @@ export function normalizeActionBrief(value, type) {
73
80
  ]));
74
81
  }
75
82
 
83
+ export function taskSpecificActionBrief(value, type) {
84
+ if (!hasCompleteActionBrief(value)) return null;
85
+ const brief = normalizeActionBrief(value, type);
86
+ const defaults = defaultActionBrief(type);
87
+ return Object.keys(defaults).every(key => brief[key] !== defaults[key]) ? brief : null;
88
+ }
89
+
76
90
  const DEFAULT_SOFTWARE_CHANGE_STAGES = Object.freeze([
77
91
  {
78
92
  id: 'triage', name: 'Triage', type: 'triage',
@@ -353,18 +367,13 @@ export function resolvePlanningWorkflowSnapshot(settings, requestedWorkItemType
353
367
  ...workflow,
354
368
  workItemType: workflow.workItemType || workflow.id,
355
369
  }));
356
- const matchingTemplate = requestedType
357
- ? actionTemplates.find(template => template.workItemType === requestedType)
358
- : null;
359
- if (matchingTemplate) return matchingTemplate;
360
-
361
370
  const catalog = actionTemplates
362
371
  .map(template => `${template.workItemType}: ${template.name} (${template.stages.map(stage => stage.type).join(' -> ')})`)
363
372
  .join('\n');
364
373
  const typeInstruction = requestedType
365
374
  ? `The user explicitly selected workItemType "${requestedType}". Keep that exact type.`
366
375
  : 'Infer one specific workItemType from the contract.';
367
- const triageInstruction = `${normalized.actionInstructions.triage}\n\n${typeInstruction}\nReusable Action templates:\n${catalog || '(none)'}\nIf the final type matches a reusable template, return its workItemType and omit actions so Work Center can apply that frozen template. Otherwise generate the smallest reliable graph of 1 to 8 Actions. Split independent work into separate Actions and declare dependsOnActionIds. Use workspaceMode read for analysis, isolated-write for independent Git changes, integrate for an integrate Action that combines isolated-write dependencies, and shared for serial side effects. Non-Git or dirty workspaces are serialized automatically. Every generated Action must state objective, approach, expectedOutcome, capability, dependencies, and workspaceMode. The objective, approach, and expectedOutcome must be specific to this WorkItem and that Action: describe the concrete work, the repository-aware execution method, and the verifiable result that will guide the executor. Generic Action-type boilerplate is invalid. Add only Actions required by this task. Do not copy a generic workflow.`;
376
+ const triageInstruction = `${normalized.actionInstructions.triage}\n\n${typeInstruction}\nReference workflow catalog:\n${catalog || '(none)'}\nUse the catalog only to understand established task categories and sequencing patterns. Always submit the smallest reliable graph of 1 to 8 task-specific Actions; never omit Actions or copy template brief text. Split independent work into separate Actions and declare dependsOnActionIds. Use workspaceMode read for analysis, isolated-write for independent Git changes, integrate for an integrate Action that combines isolated-write dependencies, and shared for serial side effects. Non-Git or dirty workspaces are serialized automatically. Every generated Action must state objective, approach, expectedOutcome, capability, dependencies, and workspaceMode. The objective, approach, and expectedOutcome must be specific to this WorkItem and that Action: describe the concrete work, the repository-aware execution method, and the verifiable result that will guide the executor. Generic Action-type boilerplate is invalid. Add only Actions required by this task. Do not copy a generic workflow.`;
368
377
  return normalizeWorkflowDefinition({
369
378
  id: 'ai-planned',
370
379
  name: 'AI planned',
@@ -434,25 +443,8 @@ export function applyGeneratedPlan(workItem, rawPlan, options = {}) {
434
443
  }
435
444
  const reservedStageIds = new Set((options.reservedStageIds || [])
436
445
  .map(id => String(id || '').trim()).filter(Boolean));
437
- const reusableTemplate = source.actionTemplates.find(template => template.workItemType === workItemType);
438
- if (reusableTemplate) {
439
- const templateActions = reusableTemplate.stages.filter(stage => stage.type !== 'triage');
440
- if (templateActions.length === 0) {
441
- throw new Error(`Reusable Action template "${workItemType}" has no executable Actions`);
442
- }
443
- const reusedStageId = templateActions.find(stage => reservedStageIds.has(stage.id))?.id;
444
- if (reusedStageId) {
445
- throw new Error(`AI-planned Action id reuses historical stage identity: ${reusedStageId}`);
446
- }
447
- return normalizeWorkflowDefinition({
448
- ...source,
449
- workItemType,
450
- actionTemplates: [],
451
- stages: [source.stages[0], ...templateActions],
452
- });
453
- }
454
446
  if (!Array.isArray(rawPlan.actions) || rawPlan.actions.length < 1 || rawPlan.actions.length > 8) {
455
- throw new Error('AI-planned triage requires between 1 and 8 Actions when no reusable template exists');
447
+ throw new Error('AI-planned triage requires between 1 and 8 task-specific Actions');
456
448
  }
457
449
  const availableVpIds = Array.isArray(options.availableVpIds)
458
450
  ? new Set(options.availableVpIds.map(id => String(id || '').trim()).filter(Boolean))