mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
930344ae23
* 🧪 chore(local-testing): add agent-gateway probe scripts for stream SoT validation Probe + tab-switch + analyzer scripts under .agents/skills/local-testing/scripts/agent-gateway/ to capture in-browser snapshots of the message store during gateway streaming and detect regressions where assistantGroup messages get clobbered by stale DB refetches. Used to verify LOBE-9501. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(agent-runtime): push canonical UIChatMessage snapshot at step boundaries LOBE-9501 Gateway-mode streaming previously let the client refetch from DB on every step_complete or tab-focus; with stream chunks landing before the DB write fans out, the refetch returned a stale assistant placeholder that clobbered the in-memory streamed assistantGroup (reasoning / tool calls / content). Server now attaches the canonical UIChatMessage[] snapshot to step_start and agent_runtime_end events so the client can use the pushed payload as Source of Truth instead of refetching: - step_start now loads agent state first, queries messages, and attaches uiMessages to the event data when topic context is known - publishAgentRuntimeEnd signature switched to a params object (additive uiMessages field) and the coordinator resolves the snapshot through an optional uiMessagesResolver hook before publishing terminal events - AgentRuntimeService wires the resolver through a lazily-instantiated MessageService so tests without S3 env still construct cleanly - MessageService.queryMessages exposes the same read path as the message.getMessages trpc lambda (FileService postProcessUrl included) Pure additive on the wire: legacy consumers see new uiMessages field, old finalState payload unchanged. Existing call sites in agentNotify and aiAgent migrated to the params shape. Failures in the resolver fall back to publishing without uiMessages so streaming never fails the step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(agent-runtime): forward uiMessages in gateway /push-event payload LOBE-9501 GatewayStreamNotifier.publishAgentRuntimeEnd was delegating uiMessages to the inner manager (Redis SSE) but reconstructing its own push-event data object that only carried { errorType, finalState, reason, reasonDetail }. In gateway mode, clients consume /push-event rather than Redis directly, so the canonical UIChatMessage[] snapshot never reached them at terminal state — and the final step has no later step_start to carry a fresh one. Forward uiMessages via the same conditional-spread pattern used in the inner managers; add two tests covering the present/absent branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
73 lines
2.7 KiB
JavaScript
73 lines
2.7 KiB
JavaScript
// Run N round-trip tab switches with event markers timed against the probe.
|
||
//
|
||
// agent-browser --cdp 9222 eval --stdin < tab-switch.js
|
||
//
|
||
// Captures the currently-active tab as the BACK target and the rightmost
|
||
// inactive tab as the AWAY target. Both are addressed by their stable
|
||
// data-contextmenu-trigger key (NOT by visible title — the active tab's
|
||
// innerText embeds a ` · <agent name>` suffix that breaks text matching).
|
||
//
|
||
// Fires the loop in the background and returns immediately so the
|
||
// agent-browser eval doesn't have to await the full ROUND_TRIPS × DWELL_MS
|
||
// duration. Wait on the `SWITCH_LOOP_DONE` event before dumping.
|
||
//
|
||
// Refuses to launch if a previous loop is still in flight.
|
||
//
|
||
// Requires probe.js to have been installed first (provides
|
||
// window.__PROBE_EVENT / __listTabs / __clickTabByKey / __activeTabKey).
|
||
|
||
(function () {
|
||
const ROUND_TRIPS = 4;
|
||
const DWELL_MS = 10_000;
|
||
|
||
if (!window.__PROBE_EVENT || !window.__listTabs || !window.__clickTabByKey) {
|
||
return 'probe not installed — eval probe.js first';
|
||
}
|
||
if (window.__SWITCH_LOOP_RUNNING) {
|
||
return 'switch loop already running — wait for SWITCH_LOOP_DONE first';
|
||
}
|
||
|
||
const tabs = window.__listTabs();
|
||
const activeTab = tabs.find((t) => t.active);
|
||
if (!activeTab) return 'no active tab — abort';
|
||
|
||
// Pick the first inactive tab as AWAY target. With multiple inactive tabs
|
||
// you'll usually want the one that's stable across the test — feel free
|
||
// to swap to tabs[tabs.length-1] if you want the rightmost.
|
||
const inactives = tabs.filter((t) => !t.active);
|
||
if (inactives.length === 0) return 'no inactive tab to switch to — abort';
|
||
const awayTab = inactives.at(-1); // rightmost inactive
|
||
|
||
const BACK_KEY = activeTab.key;
|
||
const AWAY_KEY = awayTab.key;
|
||
|
||
window.__SWITCH_LOOP_RUNNING = true;
|
||
window.__PROBE_EVENT('SWITCH_LOOP_CONFIG:back=' + BACK_KEY + ',away=' + AWAY_KEY);
|
||
|
||
(async function () {
|
||
function sleep(ms) {
|
||
return new Promise((r) => setTimeout(r, ms));
|
||
}
|
||
|
||
try {
|
||
window.__PROBE_EVENT('SWITCH_LOOP_START');
|
||
for (let i = 1; i <= ROUND_TRIPS; i++) {
|
||
window.__PROBE_EVENT('AWAY_' + i);
|
||
const awayResult = window.__clickTabByKey(AWAY_KEY);
|
||
window.__PROBE_EVENT('AWAY_' + i + '_RES:' + awayResult.slice(0, 50));
|
||
await sleep(DWELL_MS);
|
||
|
||
window.__PROBE_EVENT('BACK_' + i);
|
||
const backResult = window.__clickTabByKey(BACK_KEY);
|
||
window.__PROBE_EVENT('BACK_' + i + '_RES:' + backResult.slice(0, 50));
|
||
await sleep(DWELL_MS);
|
||
}
|
||
window.__PROBE_EVENT('SWITCH_LOOP_DONE');
|
||
} finally {
|
||
window.__SWITCH_LOOP_RUNNING = false;
|
||
}
|
||
})();
|
||
|
||
return 'switch loop kicked off (BACK=' + BACK_KEY + ', AWAY=' + AWAY_KEY + ')';
|
||
})();
|