feat(anchor): project settled final answer through anchor
This commit is contained in:
1 parent
c2ecb47554
commit
b990bd09dd
6 files changed
+275
-14
No files matched your search
@@ -21,9 +21,12 @@ streaming or rendering yet.
|
|||||||
anchor seed excludes renderer presentation state, terminal states are exposed
|
anchor seed excludes renderer presentation state, terminal states are exposed
|
||||||
as constants with alias normalization, and replay + settlement ordering is
|
as constants with alias normalization, and replay + settlement ordering is
|
||||||
pinned by tests before visible wiring begins.
|
pinned by tests before visible wiring begins.
|
||||||
- The next independently reviewable boundary is Phase 3 settlement through the
|
- Slice 4 starts RFC Phase 3 by routing settled assistant final prose through the
|
||||||
anchor owner. `S.messages`, `INFLIGHT`, stream-local state, and DOM nodes remain
|
anchor owner before `renderMessages()` renders the final assistant body.
|
||||||
projection/cache layers until that wiring lands.
|
- The next independently reviewable boundary is activity/render-scene projection
|
||||||
|
for reasoning, tool rows, and transparent-stream/worklog metadata. `S.messages`,
|
||||||
|
`INFLIGHT`, stream-local state, and DOM nodes remain projection/cache layers
|
||||||
|
outside the settled final-prose path.
|
||||||
|
|
||||||
## State Layers
|
## State Layers
|
||||||
|
|
||||||
@@ -114,6 +117,25 @@ event identity, lifecycle, final answer reference, and activity events.
|
|||||||
until the matching field is explicitly moved. The fallback order is journal
|
until the matching field is explicitly moved. The fallback order is journal
|
||||||
replay first, settled transcript second, `INFLIGHT` only for gaps.
|
replay first, settled transcript second, `INFLIGHT` only for gaps.
|
||||||
|
|
||||||
|
## Slice 4 Settled Final Projection
|
||||||
|
|
||||||
|
`HermesAssistantTurnAnchors.projectAssistantTurnAnchorSettledMessageFinalAnswer()`
|
||||||
|
projects one settled assistant transcript message through a local anchor
|
||||||
|
registry. The settled transcript message reference remains the semantic
|
||||||
|
authority (`content.final_message_ref`); `content.final_answer` is a derived
|
||||||
|
render snapshot for the existing markdown pipeline.
|
||||||
|
|
||||||
|
`renderMessages()` uses that projection only for settled assistant messages
|
||||||
|
(`!isUser && !m._live`) and only after preserving the current content-array
|
||||||
|
flattening behavior. It then continues through the existing inline-thinking and
|
||||||
|
markdown rendering pipeline. If the anchor helper is unavailable or cannot
|
||||||
|
produce a final answer, `renderMessages()` falls back to the existing message
|
||||||
|
content path.
|
||||||
|
|
||||||
|
This is intentionally narrower than render-scene ownership: live stream tokens,
|
||||||
|
replay hydration, worklog rows, transparent-stream rows, tool cards, `INFLIGHT`,
|
||||||
|
and DOM continuity are still not consumed by the anchor registry in this slice.
|
||||||
|
|
||||||
## Source Event Classification
|
## Source Event Classification
|
||||||
|
|
||||||
Phase 0 classifies current sources before changing render behavior:
|
Phase 0 classifies current sources before changing render behavior:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// Stable Assistant Turn Anchors scaffold (#3926).
|
// Stable Assistant Turn Anchors scaffold (#3926).
|
||||||
//
|
//
|
||||||
// This file is intentionally inert: it defines the current ownership inventory,
|
// This file defines the current ownership inventory, event classifications, and
|
||||||
// event classifications, and small owner helpers, but it does not register
|
// small owner helpers. It does not register anchors globally. The only renderer
|
||||||
// anchors globally or change any renderer. Later phases can wire these helpers into
|
// wiring in this slice is settled assistant final-answer projection; live
|
||||||
// send(), attachLiveStream(), replay hydration, and renderMessages().
|
// streaming, replay hydration, tools, and DOM ownership remain unwired.
|
||||||
(function(){
|
(function(){
|
||||||
const ROOT=(typeof window!=='undefined')?window:globalThis;
|
const ROOT=(typeof window!=='undefined')?window:globalThis;
|
||||||
|
|
||||||
@@ -696,6 +696,96 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _contextValue(context, keys){
|
||||||
|
return _firstOwn(context&&typeof context==='object'?context:{},keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _messageValue(message, keys){
|
||||||
|
return _firstOwn(message&&typeof message==='object'?message:{},keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _rawIndexMessageRef(context){
|
||||||
|
const rawIdx=_contextValue(context,['raw_idx','rawIdx']);
|
||||||
|
if(rawIdx===undefined||rawIdx===null||rawIdx==='') return '';
|
||||||
|
return 'raw_idx:'+String(rawIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectAssistantTurnAnchorSettledMessageFinalAnswer(input, context){
|
||||||
|
const message=(input&&typeof input==='object')?input:{};
|
||||||
|
const ctx=(context&&typeof context==='object')?context:{};
|
||||||
|
const sessionId=_cleanString(_contextValue(ctx,['session_id','sessionId']))
|
||||||
|
||_cleanString(_messageValue(message,['session_id','sessionId']));
|
||||||
|
if(!sessionId){
|
||||||
|
return Object.freeze({applied:false,reason:'missing_session',final_answer:'',final_message_ref:null,registry:null});
|
||||||
|
}
|
||||||
|
const role=_cleanString(_messageValue(message,['role']))||'assistant';
|
||||||
|
if(role&&role!=='assistant'){
|
||||||
|
return Object.freeze({applied:false,reason:'non_assistant',final_answer:'',final_message_ref:null,registry:null});
|
||||||
|
}
|
||||||
|
const runId=_cleanString(_contextValue(ctx,['run_id','runId']))
|
||||||
|
||_cleanString(_messageValue(message,['run_id','runId','_run_id','runtime_run_id']));
|
||||||
|
const streamId=_cleanString(_contextValue(ctx,['stream_id','streamId']))
|
||||||
|
||_cleanString(_messageValue(message,['stream_id','streamId','_stream_id']));
|
||||||
|
const messageRef=_firstTextValue(
|
||||||
|
_messageValue(message,['message_id','id','local_id']),
|
||||||
|
_contextValue(ctx,['message_id','messageId','local_id','localId'])
|
||||||
|
)||_rawIndexMessageRef(ctx);
|
||||||
|
const turnId=_cleanString(_contextValue(ctx,['turn_id','turnId']))
|
||||||
|
||_cleanString(_messageValue(message,['turn_id','turnId']))
|
||||||
|
||[
|
||||||
|
'settled',
|
||||||
|
sessionId,
|
||||||
|
runId||streamId||messageRef||'assistant',
|
||||||
|
].join(':');
|
||||||
|
const registry=createAssistantTurnAnchorRegistry({
|
||||||
|
session_id:sessionId,
|
||||||
|
turn_id:turnId,
|
||||||
|
run_id:runId||null,
|
||||||
|
stream_id:streamId||null,
|
||||||
|
local_id:messageRef||null,
|
||||||
|
source_message_refs:messageRef?[messageRef]:[],
|
||||||
|
});
|
||||||
|
const payload={
|
||||||
|
role:'assistant',
|
||||||
|
id:messageRef||null,
|
||||||
|
content:_hasOwn(ctx,'content')?_own(ctx,'content'):_own(message,'content'),
|
||||||
|
};
|
||||||
|
const usage=_own(message,'usage');
|
||||||
|
const turnUsage=_own(message,'_turnUsage');
|
||||||
|
if(usage&&typeof usage==='object') payload.usage=usage;
|
||||||
|
if(turnUsage&&typeof turnUsage==='object') payload._turnUsage=turnUsage;
|
||||||
|
const result=applyAssistantTurnAnchorSourceEvent(registry,{
|
||||||
|
source_type:'settled_message',
|
||||||
|
payload,
|
||||||
|
local_id:messageRef||null,
|
||||||
|
},{
|
||||||
|
session_id:sessionId,
|
||||||
|
turn_id:turnId,
|
||||||
|
run_id:runId||null,
|
||||||
|
stream_id:streamId||null,
|
||||||
|
});
|
||||||
|
if(!result.applied){
|
||||||
|
return Object.freeze({
|
||||||
|
applied:false,
|
||||||
|
reason:result.reason||null,
|
||||||
|
final_answer:'',
|
||||||
|
final_message_ref:null,
|
||||||
|
registry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const rawFinalAnswer=registry.anchor&®istry.anchor.content&®istry.anchor.content.final_answer;
|
||||||
|
const rawFinalMessageRef=registry.anchor&®istry.anchor.content&®istry.anchor.content.final_message_ref;
|
||||||
|
const finalAnswer=typeof rawFinalAnswer==='string'?rawFinalAnswer:'';
|
||||||
|
const finalMessageRef=typeof rawFinalMessageRef==='string'?rawFinalMessageRef:null;
|
||||||
|
return Object.freeze({
|
||||||
|
applied:!!result.applied,
|
||||||
|
reason:result.reason||null,
|
||||||
|
final_answer:finalAnswer,
|
||||||
|
final_message_ref:finalMessageRef,
|
||||||
|
registry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createAssistantTurnAnchorSeed(input){
|
function createAssistantTurnAnchorSeed(input){
|
||||||
const opts=(input&&typeof input==='object')?input:{};
|
const opts=(input&&typeof input==='object')?input:{};
|
||||||
const sessionId=_cleanString(opts.session_id);
|
const sessionId=_cleanString(opts.session_id);
|
||||||
@@ -755,7 +845,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
ROOT.HermesAssistantTurnAnchors=Object.freeze({
|
ROOT.HermesAssistantTurnAnchors=Object.freeze({
|
||||||
version:'slice3-registry-shadow',
|
version:'slice4-final-projection',
|
||||||
activityEventKinds:ACTIVITY_EVENT_KINDS,
|
activityEventKinds:ACTIVITY_EVENT_KINDS,
|
||||||
stateLayers:STATE_LAYERS,
|
stateLayers:STATE_LAYERS,
|
||||||
sourceEventClassification:SOURCE_EVENT_CLASSIFICATION,
|
sourceEventClassification:SOURCE_EVENT_CLASSIFICATION,
|
||||||
@@ -772,6 +862,7 @@
|
|||||||
applyAssistantTurnAnchorSourceEvent,
|
applyAssistantTurnAnchorSourceEvent,
|
||||||
applyAssistantTurnAnchorSourceEvents,
|
applyAssistantTurnAnchorSourceEvents,
|
||||||
createAssistantTurnAnchorShadowSnapshot,
|
createAssistantTurnAnchorShadowSnapshot,
|
||||||
|
projectAssistantTurnAnchorSettledMessageFinalAnswer,
|
||||||
isAssistantTurnAnchorActivityKind,
|
isAssistantTurnAnchorActivityKind,
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
@@ -8747,6 +8747,26 @@ function _renderMessagesWithScrollSnapshot(options){
|
|||||||
renderMessages({...(options||{}),preserveScroll:true});
|
renderMessages({...(options||{}),preserveScroll:true});
|
||||||
_restoreMessageScrollSnapshotSameFrame(scrollSnapshot);
|
_restoreMessageScrollSnapshotSameFrame(scrollSnapshot);
|
||||||
}
|
}
|
||||||
|
let _assistantTurnAnchorSettledFinalAnswerWarned=false;
|
||||||
|
function _assistantTurnAnchorSettledFinalAnswer(message, content, context){
|
||||||
|
try{
|
||||||
|
const api=(typeof window!=='undefined')?window.HermesAssistantTurnAnchors:null;
|
||||||
|
if(!api||typeof api.projectAssistantTurnAnchorSettledMessageFinalAnswer!=='function') return null;
|
||||||
|
const result=api.projectAssistantTurnAnchorSettledMessageFinalAnswer(message,{
|
||||||
|
session_id:context&&context.session_id,
|
||||||
|
raw_idx:context&&context.raw_idx,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
const finalAnswer=result&&typeof result.final_answer==='string'?result.final_answer:'';
|
||||||
|
return finalAnswer?finalAnswer:null;
|
||||||
|
}catch(err){
|
||||||
|
if(!_assistantTurnAnchorSettledFinalAnswerWarned&&typeof console!=='undefined'&&console.warn){
|
||||||
|
_assistantTurnAnchorSettledFinalAnswerWarned=true;
|
||||||
|
console.warn('assistant turn anchor settled-final projection failed',err);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
function _scrollAfterMessageRender(preserveScroll, scrollSnapshot){
|
function _scrollAfterMessageRender(preserveScroll, scrollSnapshot){
|
||||||
// Terminal stream renders can happen after S.activeStreamId is cleared.
|
// Terminal stream renders can happen after S.activeStreamId is cleared.
|
||||||
// In that case, preserveScroll asks the normal pin-state helper to decide:
|
// In that case, preserveScroll asks the normal pin-state helper to decide:
|
||||||
@@ -9077,6 +9097,13 @@ function renderMessages(options){
|
|||||||
if(Array.isArray(content)){
|
if(Array.isArray(content)){
|
||||||
content=content.filter(p=>p&&p.type==='text').map(p=>p.text||p.content||'').join('\n');
|
content=content.filter(p=>p&&p.type==='text').map(p=>p.text||p.content||'').join('\n');
|
||||||
}
|
}
|
||||||
|
if(m.role==='assistant'&&!m._live&&typeof content==='string'){
|
||||||
|
const anchorFinal=_assistantTurnAnchorSettledFinalAnswer(m, content, {
|
||||||
|
session_id:sid,
|
||||||
|
raw_idx:rawIdx,
|
||||||
|
});
|
||||||
|
if(anchorFinal!==null) content=anchorFinal;
|
||||||
|
}
|
||||||
if(typeof content==='string'){
|
if(typeof content==='string'){
|
||||||
if(typeof window!=='undefined'&&typeof window._extractInlineThinkingFromContentForRender==='function'){
|
if(typeof window!=='undefined'&&typeof window._extractInlineThinkingFromContentForRender==='function'){
|
||||||
const split=window._extractInlineThinkingFromContentForRender(content, thinkingText);
|
const split=window._extractInlineThinkingFromContentForRender(content, thinkingText);
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ def test_normalizer_maps_live_and_replay_to_same_anchor_event_identity():
|
|||||||
live = data["liveToken"]
|
live = data["liveToken"]
|
||||||
replay = data["replayToken"]
|
replay = data["replayToken"]
|
||||||
|
|
||||||
assert data["version"] == "slice3-registry-shadow"
|
assert data["version"] == "slice4-final-projection"
|
||||||
assert live["classification"] == "activity"
|
assert live["classification"] == "activity"
|
||||||
assert live["dedupe_key"] == 'event_id:"run-1:7"'
|
assert live["dedupe_key"] == 'event_id:"run-1:7"'
|
||||||
assert replay["dedupe_key"] == live["dedupe_key"]
|
assert replay["dedupe_key"] == live["dedupe_key"]
|
||||||
|
|||||||
@@ -88,8 +88,11 @@ def test_phase0_scaffold_is_loaded_before_current_rendering_modules():
|
|||||||
messages_pos = html.index('static/messages.js?v=__WEBUI_VERSION__')
|
messages_pos = html.index('static/messages.js?v=__WEBUI_VERSION__')
|
||||||
|
|
||||||
assert anchor_pos < ui_pos < sessions_pos < messages_pos
|
assert anchor_pos < ui_pos < sessions_pos < messages_pos
|
||||||
|
ui_src = _read(UI_JS)
|
||||||
assert "'./static/assistant_turn_anchors.js' + VQ" in _read(SW_JS)
|
assert "'./static/assistant_turn_anchors.js' + VQ" in _read(SW_JS)
|
||||||
assert "HermesAssistantTurnAnchors" not in _read(UI_JS)
|
assert "projectAssistantTurnAnchorSettledMessageFinalAnswer" in ui_src
|
||||||
|
assert "createAssistantTurnAnchorRegistry" not in ui_src
|
||||||
|
assert "applyAssistantTurnAnchorSourceEvent" not in ui_src
|
||||||
assert "HermesAssistantTurnAnchors" not in _read(SESSIONS_JS)
|
assert "HermesAssistantTurnAnchors" not in _read(SESSIONS_JS)
|
||||||
assert "HermesAssistantTurnAnchors" not in _read(MESSAGES_JS)
|
assert "HermesAssistantTurnAnchors" not in _read(MESSAGES_JS)
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,59 @@ console.log(JSON.stringify({{
|
|||||||
return json.loads(result.stdout)
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def _final_projection_snapshot() -> dict:
|
||||||
|
assert NODE, "node is required for assistant_turn_anchors.js registry tests"
|
||||||
|
script = f"""
|
||||||
|
const fs = require('fs');
|
||||||
|
const vm = require('vm');
|
||||||
|
const src = fs.readFileSync({json.dumps(str(ANCHORS_JS))}, 'utf8');
|
||||||
|
const sandbox = {{window:{{}}}};
|
||||||
|
vm.createContext(sandbox);
|
||||||
|
vm.runInContext(src, sandbox, {{filename:'assistant_turn_anchors.js'}});
|
||||||
|
const api = sandbox.window.HermesAssistantTurnAnchors;
|
||||||
|
const projected = api.projectAssistantTurnAnchorSettledMessageFinalAnswer({{
|
||||||
|
role:'assistant',
|
||||||
|
id:'message-final',
|
||||||
|
content:'raw content should be replaced by render-preserved content',
|
||||||
|
_turnUsage:{{input_tokens:8, output_tokens:13}},
|
||||||
|
}}, {{
|
||||||
|
session_id:'sid-project',
|
||||||
|
raw_idx:7,
|
||||||
|
content:'line one\\nline two',
|
||||||
|
}});
|
||||||
|
const projectedByRawIdx = api.projectAssistantTurnAnchorSettledMessageFinalAnswer({{
|
||||||
|
role:'assistant',
|
||||||
|
content:'message without id',
|
||||||
|
}}, {{
|
||||||
|
session_id:'sid-project',
|
||||||
|
raw_idx:11,
|
||||||
|
content:'raw index final',
|
||||||
|
}});
|
||||||
|
const missingSession = api.projectAssistantTurnAnchorSettledMessageFinalAnswer({{
|
||||||
|
role:'assistant',
|
||||||
|
id:'message-missing-session',
|
||||||
|
content:'final',
|
||||||
|
}}, {{}});
|
||||||
|
const nonAssistant = api.projectAssistantTurnAnchorSettledMessageFinalAnswer({{
|
||||||
|
role:'user',
|
||||||
|
id:'message-user',
|
||||||
|
content:'user text',
|
||||||
|
}}, {{
|
||||||
|
session_id:'sid-project',
|
||||||
|
}});
|
||||||
|
console.log(JSON.stringify({{
|
||||||
|
version:api.version,
|
||||||
|
projected,
|
||||||
|
projectedByRawIdx,
|
||||||
|
missingSession,
|
||||||
|
nonAssistant,
|
||||||
|
}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run([NODE, "-e", script], text=True, capture_output=True, check=False)
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
|
||||||
def _hardening_snapshot() -> dict:
|
def _hardening_snapshot() -> dict:
|
||||||
assert NODE, "node is required for assistant_turn_anchors.js registry tests"
|
assert NODE, "node is required for assistant_turn_anchors.js registry tests"
|
||||||
script = f"""
|
script = f"""
|
||||||
@@ -289,7 +342,7 @@ def test_registry_owns_one_anchor_and_dedupes_live_plus_replay_events():
|
|||||||
registry = data["registry"]
|
registry = data["registry"]
|
||||||
anchor = registry["anchor"]
|
anchor = registry["anchor"]
|
||||||
|
|
||||||
assert data["version"] == "slice3-registry-shadow"
|
assert data["version"] == "slice4-final-projection"
|
||||||
assert [item["reason"] for item in data["results"][:2]] == [None, "duplicate"]
|
assert [item["reason"] for item in data["results"][:2]] == [None, "duplicate"]
|
||||||
assert registry["event_index"]["dedupe_keys"][:2] == [
|
assert registry["event_index"]["dedupe_keys"][:2] == [
|
||||||
'event_id:"run-1:1"',
|
'event_id:"run-1:1"',
|
||||||
@@ -389,7 +442,7 @@ def test_registry_does_not_destructively_dedupe_seqless_local_tool_lifecycle():
|
|||||||
registry = data["toolRegistry"]
|
registry = data["toolRegistry"]
|
||||||
anchor = registry["anchor"]
|
anchor = registry["anchor"]
|
||||||
|
|
||||||
assert data["version"] == "slice3-registry-shadow"
|
assert data["version"] == "slice4-final-projection"
|
||||||
assert data["toolResults"] == [
|
assert data["toolResults"] == [
|
||||||
{"applied": True, "reason": None},
|
{"applied": True, "reason": None},
|
||||||
{"applied": True, "reason": None},
|
{"applied": True, "reason": None},
|
||||||
@@ -439,7 +492,7 @@ def test_shadow_snapshot_feeds_current_source_families_into_one_registry_owner()
|
|||||||
registry = data["registry"]
|
registry = data["registry"]
|
||||||
anchor = registry["anchor"]
|
anchor = registry["anchor"]
|
||||||
|
|
||||||
assert data["version"] == "slice3-registry-shadow"
|
assert data["version"] == "slice4-final-projection"
|
||||||
assert data["results"]["live"] == [{"applied": True, "reason": None}]
|
assert data["results"]["live"] == [{"applied": True, "reason": None}]
|
||||||
assert data["results"]["replay"] == [
|
assert data["results"]["replay"] == [
|
||||||
{"applied": False, "reason": "duplicate"},
|
{"applied": False, "reason": "duplicate"},
|
||||||
@@ -463,6 +516,70 @@ def test_shadow_snapshot_feeds_current_source_families_into_one_registry_owner()
|
|||||||
assert anchor["content"]["final_answer"] == "shadow final"
|
assert anchor["content"]["final_answer"] == "shadow final"
|
||||||
|
|
||||||
|
|
||||||
|
def test_final_projection_routes_settled_assistant_message_through_anchor_owner():
|
||||||
|
data = _final_projection_snapshot()
|
||||||
|
projected = data["projected"]
|
||||||
|
registry = projected["registry"]
|
||||||
|
anchor = registry["anchor"]
|
||||||
|
|
||||||
|
assert data["version"] == "slice4-final-projection"
|
||||||
|
assert projected["applied"] is True
|
||||||
|
assert projected["reason"] is None
|
||||||
|
assert projected["final_message_ref"] == "message-final"
|
||||||
|
assert projected["final_answer"] == "line one\nline two"
|
||||||
|
assert registry["stats"]["applied"] == 1
|
||||||
|
assert anchor["identity"]["session_id"] == "sid-project"
|
||||||
|
assert anchor["content"]["final_answer"] == "line one\nline two"
|
||||||
|
assert anchor["content"]["final_message_ref"] == "message-final"
|
||||||
|
assert anchor["usage"] == {"input_tokens": 8, "output_tokens": 13}
|
||||||
|
assert [event["source_event_type"] for event in anchor["metadata_events"]] == [
|
||||||
|
"settled_message",
|
||||||
|
]
|
||||||
|
assert data["projectedByRawIdx"]["final_message_ref"] == "raw_idx:11"
|
||||||
|
assert data["projectedByRawIdx"]["registry"]["anchor"]["identity"][
|
||||||
|
"source_message_refs"
|
||||||
|
] == ["raw_idx:11"]
|
||||||
|
anchor_src = _read(ANCHORS_JS)
|
||||||
|
assert "if(!result.applied)" in anchor_src
|
||||||
|
|
||||||
|
|
||||||
|
def test_final_projection_is_scoped_to_settled_assistant_messages():
|
||||||
|
data = _final_projection_snapshot()
|
||||||
|
|
||||||
|
assert data["missingSession"] == {
|
||||||
|
"applied": False,
|
||||||
|
"reason": "missing_session",
|
||||||
|
"final_answer": "",
|
||||||
|
"final_message_ref": None,
|
||||||
|
"registry": None,
|
||||||
|
}
|
||||||
|
assert data["nonAssistant"] == {
|
||||||
|
"applied": False,
|
||||||
|
"reason": "non_assistant",
|
||||||
|
"final_answer": "",
|
||||||
|
"final_message_ref": None,
|
||||||
|
"registry": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_messages_uses_anchor_projection_only_for_settled_final_prose():
|
||||||
|
src = _read(UI_JS)
|
||||||
|
start = src.index("function renderMessages")
|
||||||
|
end = src.index("function _toolDisplayName", start)
|
||||||
|
render_body = src[start:end]
|
||||||
|
|
||||||
|
flatten_idx = render_body.index("content=content.filter(p=>p&&p.type==='text')")
|
||||||
|
projection_idx = render_body.index("_assistantTurnAnchorSettledFinalAnswer(m, content")
|
||||||
|
thinking_idx = render_body.index("_extractInlineThinkingFromContentForRender(content")
|
||||||
|
|
||||||
|
assert flatten_idx < projection_idx < thinking_idx
|
||||||
|
assert "if(m.role==='assistant'&&!m._live&&typeof content==='string'){" in render_body
|
||||||
|
assert "createAssistantTurnAnchorRegistry" not in render_body
|
||||||
|
assert "applyAssistantTurnAnchorSourceEvent" not in render_body
|
||||||
|
assert "_assistantTurnAnchorSettledFinalAnswerWarned" in src
|
||||||
|
assert "console.warn('assistant turn anchor settled-final projection failed',err)" in src
|
||||||
|
|
||||||
|
|
||||||
def test_registry_instances_do_not_share_owner_state():
|
def test_registry_instances_do_not_share_owner_state():
|
||||||
data = _registry_snapshot()
|
data = _registry_snapshot()
|
||||||
isolated = data["isolated"]
|
isolated = data["isolated"]
|
||||||
@@ -473,7 +590,7 @@ def test_registry_instances_do_not_share_owner_state():
|
|||||||
assert isolated["anchor"]["activity_events"] == []
|
assert isolated["anchor"]["activity_events"] == []
|
||||||
|
|
||||||
|
|
||||||
def test_slice3_registry_is_still_unwired_from_rendering_hot_paths():
|
def test_slice4_projection_does_not_wire_registry_into_rendering_hot_paths():
|
||||||
helper_names = [
|
helper_names = [
|
||||||
"createAssistantTurnAnchorRegistry",
|
"createAssistantTurnAnchorRegistry",
|
||||||
"applyAssistantTurnAnchorNormalizedEvent",
|
"applyAssistantTurnAnchorNormalizedEvent",
|
||||||
@@ -485,3 +602,4 @@ def test_slice3_registry_is_still_unwired_from_rendering_hot_paths():
|
|||||||
assert helper not in _read(UI_JS)
|
assert helper not in _read(UI_JS)
|
||||||
assert helper not in _read(SESSIONS_JS)
|
assert helper not in _read(SESSIONS_JS)
|
||||||
assert helper not in _read(MESSAGES_JS)
|
assert helper not in _read(MESSAGES_JS)
|
||||||
|
assert "projectAssistantTurnAnchorSettledMessageFinalAnswer" in _read(UI_JS)
|
||||||
Reference in new issue
Block a user