fix(streaming): complete interim anchor prose

Treat status-less interim assistant events as complete semantic boundaries so streaming Markdown flushes its final lookahead character before tool activity. Add behavior coverage for the real interim-to-tool Anchor pipeline.

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.3-codex-spark
This commit is contained in:
starship-s committed 2026-07-24 12:30:46 -06:00
1 parent 399cd7ab19
commit 69b26c1fa5
2 files changed
+69

No files matched your search

+1
View File
@@ -302,6 +302,7 @@
?normalizeAssistantTurnAnchorTerminalState(explicit,sourceType)||explicit
:explicit;
}
if(sourceType==='interim_assistant') return 'completed'; // interim prose is a complete semantic boundary, unlike token deltas.
if(kind==='tool_started') return 'running';
if(kind==='tool_completed') return _own(payload,'is_error')?'error':'completed';
if(kind==='terminal_status'){
@@ -48,8 +48,10 @@ function extractFn(src, name){
HARNESS_JS_TEMPLATE = r"""
const fs = require('fs');
const vm = require('vm');
const msgSrc = fs.readFileSync(__MSG_SRC__, 'utf8');
const uiSrc = fs.readFileSync(__UI_SRC__, 'utf8');
const anchorSrc = fs.readFileSync(__ANCHOR_SRC__, 'utf8');
let _extractSource = msgSrc;
let _anchorProseSmdCache;
let _anchorProseIncrementalNode;
@@ -215,6 +217,11 @@ function createHarness(options={}){
global.renderMd = (text)=>String(text || '');
global.esc = (value)=>String(value || '');
vm.runInThisContext(anchorSrc, {filename:'assistant_turn_anchors.js'});
if(!window.HermesAssistantTurnAnchors){
throw new Error('assistant_turn_anchors.js did not expose HermesAssistantTurnAnchors');
}
_anchorProseSmdCache = cache;
window._anchorProseSmdCache = cache;
_extractSource = msgSrc;
@@ -227,6 +234,7 @@ function createHarness(options={}){
return {
calls,
cache,
anchorApi: window.HermesAssistantTurnAnchors,
renderRow(row){
return _anchorSceneNodeForRow(row, {settled:false});
},
@@ -283,6 +291,54 @@ function runCase(name){
};
}
if(name === 'interim_assistant_tool_boundary_keeps_interim_prose_authoritative_full_text'){
const registry = harness.anchorApi.createAssistantTurnAnchorRegistry({
session_id:'session-final-char',
turn_id:'turn-final-char',
run_id:'run-final-char',
stream_id:'stream-final-char',
local_id:'assistant-msg-final-char',
source_message_refs:['assistant-msg-final-char'],
});
const context = {
session_id:'session-final-char',
turn_id:'turn-final-char',
run_id:'run-final-char',
stream_id:'stream-final-char',
};
const authoritativeText = 'Streaming with punctuation.';
harness.anchorApi.applyAssistantTurnAnchorSourceEvent(registry,{
source_event_type:'interim_assistant',
seq:1,
local_id:'prose-final-char',
payload:{text:authoritativeText},
...context,
});
harness.anchorApi.applyAssistantTurnAnchorSourceEvent(registry,{
source_event_type:'tool',
seq:2,
local_id:'tool-final-char',
payload:{text:'tool started', name:'search', tool_call_id:'tool-call-1'},
...context,
});
const scene = harness.anchorApi.projectAssistantTurnAnchorActivityScene(registry,{mode:'compact_worklog'});
const rows = Array.isArray(scene && scene.activity_rows) ? scene.activity_rows : [];
const proseActivityRow = rows.find((row)=>row.role === 'prose') || null;
const toolActivityRow = rows.find((row)=>row.role === 'tool') || null;
const proseNode = proseActivityRow ? harness.renderRow(proseActivityRow) : null;
const proseBody = proseNode && proseNode.querySelector('.msg-body');
return {
sceneRowsCount: rows.length,
proseStatus: proseActivityRow ? proseActivityRow.status : null,
proseSourceText: proseActivityRow ? proseActivityRow.text : null,
proseRenderedText: proseBody ? proseBody.textContent : null,
toolStatus: toolActivityRow ? toolActivityRow.status : null,
toolSourceEventType: toolActivityRow ? toolActivityRow.source_event_type : null,
finalizeCount: harness.calls.ended,
authoritativeText,
};
}
if(name === 'tool_boundary_completes_row'){
harness.renderRow(row('running', 'pre tool'));
const node = harness.renderRow(row('completed', 'pre tool.'));
@@ -328,6 +384,7 @@ def _run_scenario(case_name: str) -> dict:
script = HARNESS_JS_TEMPLATE
script = script.replace("__MSG_SRC__", json.dumps(str(ROOT / 'static' / 'messages.js')))
script = script.replace("__UI_SRC__", json.dumps(str(ROOT / 'static' / 'ui.js')))
script = script.replace("__ANCHOR_SRC__", json.dumps(str(ROOT / 'static' / 'assistant_turn_anchors.js')))
script = script.replace("__EXTRACT_FN_JS__", EXTRACT_FN_JS)
script = script.replace("__CASE__", case)
result = subprocess.run([NODE, "-e", script], cwd=ROOT, text=True, capture_output=True, check=False)
@@ -395,3 +452,14 @@ def test_anchor_prose_parser_end_failure_falls_back_to_full_renderMd():
assert data["renderCount"] == 1
assert data["bodyText"] == "streamed final."
assert data["cacheSize"] == 0
@pytest.mark.skipif(NODE is None, reason="node is required for anchor prose incremental harness tests")
def test_interim_assistant_to_tool_boundary_must_render_full_authoritative_punctuation():
data = _run_scenario("interim_assistant_tool_boundary_keeps_interim_prose_authoritative_full_text")
assert data["sceneRowsCount"] >= 2
assert data["proseStatus"] == "completed"
assert data["toolStatus"] == "running"
assert data["toolSourceEventType"] == "tool"
assert data["proseSourceText"] == data["authoritativeText"]
assert data["proseRenderedText"] == data["authoritativeText"]