@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.
- 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 +120 -112
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- 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
- package/yeaft/work-center/projection.js +2 -2
- package/yeaft/work-center/runner.js +2 -2
- package/yeaft/work-center/workflow.js +16 -24
package/yeaft/web-bridge.js
CHANGED
|
@@ -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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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,
|
|
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 {
|
|
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 =
|
|
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}\
|
|
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
|
|
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))
|