@yeaft/webchat-agent 1.0.379 → 1.0.381
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/message-router.js +3 -3
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +4 -3
- 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/engine.js +104 -39
- package/yeaft/llm/adapter.js +15 -0
- package/yeaft/llm/router.js +83 -24
- package/yeaft/llm/usage-accounting.js +28 -2
- package/yeaft/sessions/session-config.js +0 -2
- package/yeaft/web-bridge.js +54 -21
- package/yeaft/work-center/runner.js +51 -3
- package/yeaft/work-center/store.js +3 -1
package/yeaft/web-bridge.js
CHANGED
|
@@ -175,12 +175,24 @@ function modelRefsEquivalent(left, right) {
|
|
|
175
175
|
function resolveLiveSessionConfig(baseConfig, sessionId, options = {}) {
|
|
176
176
|
const configRoot = baseConfig?.dir || liveConfigRoot();
|
|
177
177
|
const sessionConfig = normalizeSessionConfig(configRoot, sessionId, baseConfig, options);
|
|
178
|
-
|
|
178
|
+
const resolved = resolveSessionConfig(baseConfig, sessionConfig);
|
|
179
|
+
// Session config lives in the agent-local root. `resolveSessionConfig()`
|
|
180
|
+
// intentionally returns a fresh object without its storage hint, so restore
|
|
181
|
+
// it for the next cached-engine lookup.
|
|
182
|
+
return configRoot && !resolved.dir ? { ...resolved, dir: configRoot } : resolved;
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
let sessionConfigRefreshRevision = 0;
|
|
182
186
|
|
|
183
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Reload the Agent-owned config and install it into every live Engine.
|
|
189
|
+
*
|
|
190
|
+
* This deliberately mutates only runtime snapshots. Model/config saves must
|
|
191
|
+
* not retire cached engines, clear task owners, invalidate the coordinator, or
|
|
192
|
+
* abort a request that has already started. Engine.query() captures its LLM
|
|
193
|
+
* request values at each loop boundary, so an active stream completes with its
|
|
194
|
+
* original config and a following tool loop uses the published snapshot.
|
|
195
|
+
*/
|
|
184
196
|
export async function refreshLiveSessionConfig(options = {}) {
|
|
185
197
|
sessionConfigRefreshRevision += 1;
|
|
186
198
|
if (!session && sessionLoadPromise) {
|
|
@@ -591,14 +603,17 @@ const vpCurrentTodos = new Map();
|
|
|
591
603
|
* that nobody consumes — exactly the pre-707 bug).
|
|
592
604
|
*
|
|
593
605
|
* Purge sites:
|
|
594
|
-
* - `invalidateGroupContext(sessionId)` — called from
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
* auto-add / default-VP-heal pass mutated the roster.
|
|
606
|
+
* - `invalidateGroupContext(sessionId)` — called from Session CRUD handlers
|
|
607
|
+
* that mutate roster / metadata / lifecycle state on disk (rename, update
|
|
608
|
+
* announcement, archive, delete, add/remove member, set default VP).
|
|
609
|
+
* - `handleYeaftSessionSend` — invalidates inline when its own auto-add /
|
|
610
|
+
* default-VP-heal pass mutated the roster.
|
|
600
611
|
* - `resetYeaftSession` and `__testResetVpState` clear the whole map.
|
|
601
612
|
*
|
|
613
|
+
* Model/config saves deliberately do not purge this map: they publish a new
|
|
614
|
+
* in-memory runtime snapshot and the active engine adopts it at its next LLM
|
|
615
|
+
* loop boundary without aborting the request already in flight.
|
|
616
|
+
*
|
|
602
617
|
* @type {Map<string, { coord: ReturnType<typeof createCoordinator>,
|
|
603
618
|
* router: ReturnType<typeof createRouter>,
|
|
604
619
|
* sessionHandle: object }>}
|
|
@@ -1803,8 +1818,16 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
|
|
|
1803
1818
|
const effectiveConfig = resolveLiveSessionConfig(session.config, sessionId);
|
|
1804
1819
|
const configKey = engineConfigKey(effectiveConfig);
|
|
1805
1820
|
let eng = vpEngines.get(key);
|
|
1806
|
-
if (eng
|
|
1807
|
-
|
|
1821
|
+
if (eng) {
|
|
1822
|
+
// Config changes are snapshots, not a lifecycle boundary. The save handler
|
|
1823
|
+
// publishes the snapshot to every cached engine. The next query loop reads
|
|
1824
|
+
// it before invoking its adapter; no engine replacement or abort needed.
|
|
1825
|
+
if (vpEngineConfigKeys.get(key) !== configKey) {
|
|
1826
|
+
eng.refreshConfig?.(effectiveConfig);
|
|
1827
|
+
vpEngineConfigKeys.set(key, configKey);
|
|
1828
|
+
}
|
|
1829
|
+
return eng;
|
|
1830
|
+
}
|
|
1808
1831
|
eng = new Engine({
|
|
1809
1832
|
adapter: session.adapter,
|
|
1810
1833
|
trace: session.trace,
|
|
@@ -3416,12 +3439,14 @@ export function handleYeaftUpdateSession(msg) {
|
|
|
3416
3439
|
}
|
|
3417
3440
|
|
|
3418
3441
|
/**
|
|
3419
|
-
* Persist the model selected in the
|
|
3420
|
-
*
|
|
3421
|
-
*
|
|
3442
|
+
* Persist the model selected in the Session conversation header.
|
|
3443
|
+
*
|
|
3444
|
+
* Cached engines remain alive. This handler publishes their updated effective
|
|
3445
|
+
* config; `Engine.refreshConfig()` applies it at the next LLM loop boundary.
|
|
3446
|
+
* The current stream and its AbortController are untouched.
|
|
3422
3447
|
*
|
|
3423
3448
|
* Payload: { sessionId, requestId, config: { model?: string|null } }
|
|
3424
|
-
* - `model: ''` or `null` clears the selected
|
|
3449
|
+
* - `model: ''` or `null` clears the selected Session model (falls back to user default).
|
|
3425
3450
|
*/
|
|
3426
3451
|
export function handleYeaftUpdateSessionConfig(msg) {
|
|
3427
3452
|
const requestId = msg && msg.requestId;
|
|
@@ -3433,15 +3458,23 @@ export function handleYeaftUpdateSessionConfig(msg) {
|
|
|
3433
3458
|
if (!partial) throw new SessionConfigError('invalid_patch', 'config object required');
|
|
3434
3459
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
3435
3460
|
const savedConfig = updateSessionConfig(yeaftDir, sessionId, partial);
|
|
3436
|
-
|
|
3437
|
-
//
|
|
3461
|
+
const effectiveBaseConfig = session?.config || loadConfig({ dir: yeaftDir });
|
|
3462
|
+
// The write above is the source of truth. Merge it directly instead of
|
|
3463
|
+
// reopening the file so cached engines cannot observe an unrelated stale
|
|
3464
|
+
// read between persistence and publication.
|
|
3465
|
+
const sessionConfig = resolveSessionConfig(effectiveBaseConfig, savedConfig);
|
|
3466
|
+
const publishedSessionConfig = sessionConfig.dir || !effectiveBaseConfig?.dir
|
|
3467
|
+
? sessionConfig
|
|
3468
|
+
: { ...sessionConfig, dir: effectiveBaseConfig.dir };
|
|
3438
3469
|
const prefix = `${sessionId}::`;
|
|
3439
|
-
for (const
|
|
3440
|
-
if (
|
|
3441
|
-
|
|
3442
|
-
|
|
3470
|
+
for (const [key, engine] of vpEngines) {
|
|
3471
|
+
if (!key.startsWith(prefix)) continue;
|
|
3472
|
+
engine.refreshConfig?.(publishedSessionConfig);
|
|
3473
|
+
vpEngineConfigKeys.set(key, engineConfigKey(publishedSessionConfig));
|
|
3443
3474
|
}
|
|
3444
|
-
|
|
3475
|
+
// Do not invalidate the Session coordinator or abort active VP turns.
|
|
3476
|
+
// The next adapter loop sees this config; the stream already underway
|
|
3477
|
+
// completes with the values captured for that request.
|
|
3445
3478
|
sendSessionCrudResult({ op: 'update_config', requestId, ok: true, sessionId, config: savedConfig });
|
|
3446
3479
|
sendSessionSnapshotBroadcast();
|
|
3447
3480
|
} catch (err) {
|
|
@@ -1022,7 +1022,7 @@ export class WorkItemRunner {
|
|
|
1022
1022
|
}
|
|
1023
1023
|
}
|
|
1024
1024
|
|
|
1025
|
-
async run({ workItem, action, run, signal, ownerBootId, onProgress, registerProgressReader, registerInputWake }) {
|
|
1025
|
+
async run({ workItem, action, run, signal, ownerBootId, onProgress, registerProgressReader, registerInputWake, onEngineEvent = null }) {
|
|
1026
1026
|
const runtime = await this.runtimeProvider();
|
|
1027
1027
|
const currentSettings = ['ai', 'coordinator'].includes(workItem?.workflowSnapshot?.planningMode)
|
|
1028
1028
|
&& this.policyProvider ? await this.policyProvider() : null;
|
|
@@ -1307,6 +1307,7 @@ export class WorkItemRunner {
|
|
|
1307
1307
|
}
|
|
1308
1308
|
return accepted;
|
|
1309
1309
|
};
|
|
1310
|
+
let activeProviderRequest = null;
|
|
1310
1311
|
const prepareProviderRequest = ({ entries, system, messages, model }) => {
|
|
1311
1312
|
const durableEntries = entries
|
|
1312
1313
|
.filter(entry => entry?.durableInputId)
|
|
@@ -1318,6 +1319,7 @@ export class WorkItemRunner {
|
|
|
1318
1319
|
{ requestBody, dispatchCapability: 'unknown' },
|
|
1319
1320
|
);
|
|
1320
1321
|
if (!turn) throw new Error('Work Center could not persist the next provider turn');
|
|
1322
|
+
activeProviderRequest = turn;
|
|
1321
1323
|
return turn;
|
|
1322
1324
|
};
|
|
1323
1325
|
const startProviderRequest = turn => {
|
|
@@ -1330,10 +1332,12 @@ export class WorkItemRunner {
|
|
|
1330
1332
|
if (!this.store.consumeEngineTurn?.(turn.id, ownerBootId, run.leaseEpoch, result)) {
|
|
1331
1333
|
throw new Error('Work Center provider response lost its EngineTurn fence');
|
|
1332
1334
|
}
|
|
1335
|
+
if (activeProviderRequest?.id === turn.id) activeProviderRequest = null;
|
|
1333
1336
|
};
|
|
1334
1337
|
const failProviderRequest = (turn, error) => {
|
|
1335
1338
|
if (!turn) return;
|
|
1336
1339
|
const failure = this.store.failEngineTurn?.(turn.id, ownerBootId, run.leaseEpoch, error);
|
|
1340
|
+
if (activeProviderRequest?.id === turn.id) activeProviderRequest = null;
|
|
1337
1341
|
if (failure && failure.allowRetry === false) {
|
|
1338
1342
|
error.retryable = false;
|
|
1339
1343
|
error.workItemFailureKind = 'provider_dispatch_unknown';
|
|
@@ -1351,7 +1355,7 @@ export class WorkItemRunner {
|
|
|
1351
1355
|
const promptParts = attachmentContext.promptParts.length > 0
|
|
1352
1356
|
? [{ type: 'text', text: prompt }, ...attachmentContext.promptParts]
|
|
1353
1357
|
: null;
|
|
1354
|
-
|
|
1358
|
+
const query = engine.query({
|
|
1355
1359
|
prompt,
|
|
1356
1360
|
promptParts,
|
|
1357
1361
|
messages: [],
|
|
@@ -1373,7 +1377,25 @@ export class WorkItemRunner {
|
|
|
1373
1377
|
),
|
|
1374
1378
|
|
|
1375
1379
|
collabToolPolicy: 'single-vp',
|
|
1376
|
-
})
|
|
1380
|
+
});
|
|
1381
|
+
const iterator = query[Symbol.asyncIterator]();
|
|
1382
|
+
let stoppedByEngineEvent = false;
|
|
1383
|
+
let stoppedAfterDispatch = false;
|
|
1384
|
+
while (true) {
|
|
1385
|
+
const step = await iterator.next();
|
|
1386
|
+
if (step.done) break;
|
|
1387
|
+
const event = step.value;
|
|
1388
|
+
const control = await onEngineEvent?.(event, { iterator, engine, query });
|
|
1389
|
+
if (control?.stop === true) {
|
|
1390
|
+
// The durable in-flight turn, not an event label, is the authoritative
|
|
1391
|
+
// dispatch boundary. `user_append` and other pre-request events can
|
|
1392
|
+
// precede turn_start; only an active EngineTurn is unsafe to replay.
|
|
1393
|
+
stoppedAfterDispatch = Boolean(activeProviderRequest);
|
|
1394
|
+
if (stoppedAfterDispatch) engine.abort?.('work_item_consumer_stopped_after_dispatch');
|
|
1395
|
+
await iterator.return();
|
|
1396
|
+
stoppedByEngineEvent = true;
|
|
1397
|
+
break;
|
|
1398
|
+
}
|
|
1377
1399
|
if (event?.type === 'loop') {
|
|
1378
1400
|
loopCount += 1;
|
|
1379
1401
|
this.store.appendRunLoop?.(run.id, ownerBootId, run.leaseEpoch, {
|
|
@@ -1405,6 +1427,32 @@ export class WorkItemRunner {
|
|
|
1405
1427
|
// preserve Work Center's historical rejection semantics so Run fencing,
|
|
1406
1428
|
// hazardous side-effect handling, and retry policy still see the failure.
|
|
1407
1429
|
if (terminalEngineError) throw terminalEngineError;
|
|
1430
|
+
if (stoppedByEngineEvent) {
|
|
1431
|
+
const stopped = new Error(stoppedAfterDispatch
|
|
1432
|
+
? 'Work Center Engine consumer stopped after provider dispatch'
|
|
1433
|
+
: 'Work Center Engine consumer stopped before provider dispatch');
|
|
1434
|
+
stopped.name = 'WorkCenterEngineStoppedError';
|
|
1435
|
+
if (stoppedAfterDispatch) {
|
|
1436
|
+
// A visible provider event proves dispatch happened. The Engine's
|
|
1437
|
+
// iterator close cannot safely replay that request, so terminally
|
|
1438
|
+
// fence its durable turn before the watcher sees the failure.
|
|
1439
|
+
const failed = this.store.failEngineTurn?.(
|
|
1440
|
+
activeProviderRequest?.id,
|
|
1441
|
+
ownerBootId,
|
|
1442
|
+
run.leaseEpoch,
|
|
1443
|
+
stopped,
|
|
1444
|
+
);
|
|
1445
|
+
activeProviderRequest = null;
|
|
1446
|
+
stopped.retryable = false;
|
|
1447
|
+
stopped.workItemFailureKind = failed?.status === 'unknown'
|
|
1448
|
+
? 'provider_dispatch_unknown'
|
|
1449
|
+
: 'system_blocked';
|
|
1450
|
+
stopped.workItemFailureCode = failed?.status === 'unknown'
|
|
1451
|
+
? 'engine_turn_dispatch_unknown'
|
|
1452
|
+
: 'engine_turn_stop_failed';
|
|
1453
|
+
}
|
|
1454
|
+
throw stopped;
|
|
1455
|
+
}
|
|
1408
1456
|
} catch (error) {
|
|
1409
1457
|
error.workItemExecutionStats = currentProgress();
|
|
1410
1458
|
throw error;
|
|
@@ -2128,6 +2128,7 @@ export class WorkItemStore {
|
|
|
2128
2128
|
const turn = this.getEngineTurn(turnId);
|
|
2129
2129
|
if (!turn || turn.status !== 'dispatching' || turn.ownerBootId !== ownerBootId
|
|
2130
2130
|
|| turn.leaseEpoch !== leaseEpoch) return false;
|
|
2131
|
+
if (!this.#activeRunRow(turn.runId, ownerBootId, leaseEpoch, true)) return false;
|
|
2131
2132
|
const now = this.now();
|
|
2132
2133
|
const response = {
|
|
2133
2134
|
text: String(result.responseText || ''),
|
|
@@ -2161,7 +2162,8 @@ export class WorkItemStore {
|
|
|
2161
2162
|
failEngineTurn(turnId, ownerBootId, leaseEpoch, error) {
|
|
2162
2163
|
return withTransaction(this.db, () => {
|
|
2163
2164
|
const turn = this.getEngineTurn(turnId);
|
|
2164
|
-
if (!turn || turn.ownerBootId !== ownerBootId || turn.leaseEpoch !== leaseEpoch
|
|
2165
|
+
if (!turn || turn.ownerBootId !== ownerBootId || turn.leaseEpoch !== leaseEpoch
|
|
2166
|
+
|| !this.#activeRunRow(turn.runId, ownerBootId, leaseEpoch, true)) {
|
|
2165
2167
|
return { allowRetry: false, status: 'stale' };
|
|
2166
2168
|
}
|
|
2167
2169
|
if (turn.status === 'prepared') return { allowRetry: true, status: 'prepared' };
|