Preserve historical anchor final text
This commit is contained in:
1 parent
7fa8f9415d
commit
9dfe1367c7
4 files changed
+173
-10
No files matched your search
@@ -6490,6 +6490,12 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
return `${m.role}|${ts}|${body.slice(0,160)}`;
|
||||
}
|
||||
const _EPHEMERAL_TURN_FIELDS=['_turnUsage','_turnDuration','_turnTps','_gatewayRouting','_statusCard','_anchor_stream_id','_anchor_activity_scene'];
|
||||
function _isHistoricalAnchorActivityScene(scene){
|
||||
if(!scene||typeof scene!=='object') return false;
|
||||
const identity=scene.identity&&typeof scene.identity==='object'?scene.identity:null;
|
||||
const turnId=identity&&typeof identity.turn_id==='string'?identity.turn_id:'';
|
||||
return turnId.indexOf('historical:')===0;
|
||||
}
|
||||
function _carryForwardEphemeralTurnFields(prevMessages, nextMessages){
|
||||
if(!Array.isArray(prevMessages)||!Array.isArray(nextMessages)) return nextMessages;
|
||||
if(!prevMessages.length||!nextMessages.length) return nextMessages;
|
||||
@@ -6504,6 +6510,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
const k=_messageIdentityKey(nm); if(!k) continue;
|
||||
const pm=prevIdx.get(k); if(!pm) continue;
|
||||
for(const f of _EPHEMERAL_TURN_FIELDS){
|
||||
if(f==='_anchor_activity_scene'&&_isHistoricalAnchorActivityScene(pm[f])) continue;
|
||||
if(pm[f]!=null && nm[f]==null) nm[f]=pm[f];
|
||||
}
|
||||
}
|
||||
|
||||
+11
-9
@@ -14710,14 +14710,16 @@ function _toolArgsSnapshot(args, limit){
|
||||
function _idLinkedHistoricalMessageText(message){
|
||||
if(!message||typeof message!=='object') return '';
|
||||
const content=message.content;
|
||||
if(typeof content==='string') return content.trim();
|
||||
if(typeof content==='string') return content;
|
||||
if(!Array.isArray(content)) return '';
|
||||
return content.map(part=>{
|
||||
if(typeof part==='string') return part;
|
||||
return content.filter(part=>part&&typeof part==='object'&&part.type==='text').map(part=>{
|
||||
if(!part||typeof part!=='object') return '';
|
||||
if(part.type!=='text'&&part.type!=='input_text'&&part.type!=='output_text') return '';
|
||||
return String(part.text||part.content||'');
|
||||
}).join('').trim();
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
function _idLinkedHistoricalMessageHasVisibleText(message){
|
||||
return _idLinkedHistoricalMessageText(message).trim()!=='';
|
||||
}
|
||||
|
||||
function _idLinkedHistoricalMessageRef(message, rawIdx){
|
||||
@@ -14802,17 +14804,17 @@ function _idLinkedHistoricalTurnScene(messages, turnStart, turnEnd, options){
|
||||
if(message._anchor_activity_scene) return null;
|
||||
if(role==='assistant'){
|
||||
assistantIndexes.push(rawIdx);
|
||||
const visibleText=_idLinkedHistoricalMessageText(message);
|
||||
const hasVisibleText=_idLinkedHistoricalMessageHasVisibleText(message);
|
||||
const reasoningText=_assistantReasoningPayloadText(message);
|
||||
if(visibleText) visibleAssistantIndexes.push(rawIdx);
|
||||
if(hasVisibleText) visibleAssistantIndexes.push(rawIdx);
|
||||
if(reasoningText) return null;
|
||||
if(_idLinkedHistoricalHasVisibleSidecar(message)) return null;
|
||||
if(Array.isArray(message._partial_tool_calls)&&message._partial_tool_calls.length) return null;
|
||||
if(Array.isArray(message.content)&&message.content.some(part=>part&&typeof part==='object'&&part.type==='tool_use')) return null;
|
||||
const toolCalls=Array.isArray(message.tool_calls)?message.tool_calls:[];
|
||||
if(toolCalls.length&&visibleText) return null;
|
||||
if(toolCalls.length&&hasVisibleText) return null;
|
||||
if(!toolCalls.length){
|
||||
if(visibleText) continue;
|
||||
if(hasVisibleText) continue;
|
||||
return null;
|
||||
}
|
||||
if(message.content!==undefined&&message.content!==null&&message.content!=='') return null;
|
||||
|
||||
@@ -61,6 +61,7 @@ function installRuntimeHelpers() {
|
||||
"_filterRecoveryControlMessages",
|
||||
"_replaceMarkerOnlyAssistantWithStreamError",
|
||||
"_messageIdentityKey",
|
||||
"_isHistoricalAnchorActivityScene",
|
||||
"_carryForwardEphemeralTurnFields",
|
||||
"_isTerminalStreamErrorMarkerMessage",
|
||||
"_ensureSingleTerminalStreamErrorMarker",
|
||||
|
||||
@@ -83,6 +83,7 @@ for(const name of [
|
||||
'_toolArgsSnapshot',
|
||||
'_assistantReasoningPayloadText',
|
||||
'_idLinkedHistoricalMessageText',
|
||||
'_idLinkedHistoricalMessageHasVisibleText',
|
||||
'_idLinkedHistoricalMessageRef',
|
||||
'_idLinkedHistoricalToolArguments',
|
||||
'_idLinkedHistoricalToolResultRaw',
|
||||
@@ -91,7 +92,8 @@ for(const name of [
|
||||
'_idLinkedHistoricalTurnScene',
|
||||
'_hydrateIdLinkedHistoricalToolScenes',
|
||||
]){{
|
||||
vm.runInContext(extractFunc(name), sandbox, {{filename:'ui.js:' + name}});
|
||||
const fn = src.indexOf('function ' + name) >= 0 ? extractFunc(name) : null;
|
||||
if(fn) vm.runInContext(fn, sandbox, {{filename:'ui.js:' + name}});
|
||||
}}
|
||||
|
||||
if({str(throw_helper).lower()}){{
|
||||
@@ -135,6 +137,111 @@ console.log(JSON.stringify({{first, second, unchanged:afterFirst===JSON.stringif
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def _run_carry_forward_then_hydration(messages: list[dict]) -> dict:
|
||||
assert NODE, "node is required for Anchor hydration behavior tests"
|
||||
script = f"""
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
const uiSrc = fs.readFileSync({json.dumps(str(ROOT / 'static' / 'ui.js'))}, 'utf8');
|
||||
const messagesSrc = fs.readFileSync({json.dumps(str(ROOT / 'static' / 'messages.js'))}, 'utf8');
|
||||
let src = uiSrc;
|
||||
{_EXTRACT_FUNC_JS}
|
||||
|
||||
function extractFuncFrom(source, name){{
|
||||
const previous = src;
|
||||
src = source;
|
||||
try{{ return extractFunc(name); }}
|
||||
finally{{ src = previous; }}
|
||||
}}
|
||||
|
||||
const sandbox = {{console}};
|
||||
sandbox.window = sandbox;
|
||||
sandbox.globalThis = sandbox;
|
||||
sandbox._EPHEMERAL_TURN_FIELDS = [
|
||||
'_turnUsage',
|
||||
'_turnDuration',
|
||||
'_turnTps',
|
||||
'_gatewayRouting',
|
||||
'_statusCard',
|
||||
'_anchor_stream_id',
|
||||
'_anchor_activity_scene',
|
||||
];
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(
|
||||
fs.readFileSync({json.dumps(str(ANCHOR_JS))}, 'utf8'),
|
||||
sandbox,
|
||||
{{filename:'assistant_turn_anchors.js'}}
|
||||
);
|
||||
sandbox._redactToolTargetLabel = value => String(value);
|
||||
|
||||
for(const name of [
|
||||
'_clipCliToolSnippet',
|
||||
'_cliToolResultText',
|
||||
'_cliLooksLikePatchDiff',
|
||||
'_cliToolResultSnippet',
|
||||
'_prefixedCliDiffLines',
|
||||
'_firstOwnedValue',
|
||||
'_cliPatchSnippetFromArgs',
|
||||
'_cliToolCardSnippet',
|
||||
'_cliToolCardHasDiffSnippet',
|
||||
'_anchorSceneToolCallFromRow',
|
||||
'_toolArgsSnapshot',
|
||||
'_assistantReasoningPayloadText',
|
||||
'_idLinkedHistoricalMessageText',
|
||||
'_idLinkedHistoricalMessageHasVisibleText',
|
||||
'_idLinkedHistoricalMessageRef',
|
||||
'_idLinkedHistoricalToolArguments',
|
||||
'_idLinkedHistoricalToolResultRaw',
|
||||
'_idLinkedHistoricalRedactSnippet',
|
||||
'_idLinkedHistoricalHasVisibleSidecar',
|
||||
'_idLinkedHistoricalTurnScene',
|
||||
'_hydrateIdLinkedHistoricalToolScenes',
|
||||
]){{
|
||||
const fn = uiSrc.indexOf('function ' + name) >= 0 ? extractFuncFrom(uiSrc, name) : null;
|
||||
if(fn) vm.runInContext(fn, sandbox, {{filename:'ui.js:' + name}});
|
||||
}}
|
||||
for(const name of [
|
||||
'_messageIdentityKey',
|
||||
'_isHistoricalAnchorActivityScene',
|
||||
'_carryForwardEphemeralTurnFields',
|
||||
]){{
|
||||
const fn = messagesSrc.indexOf('function ' + name) >= 0 ? extractFuncFrom(messagesSrc, name) : null;
|
||||
if(fn) vm.runInContext(fn, sandbox, {{filename:'messages.js:' + name}});
|
||||
}}
|
||||
|
||||
const original = {json.dumps(messages)};
|
||||
const prev = JSON.parse(JSON.stringify(original));
|
||||
sandbox.prev = prev;
|
||||
vm.runInContext(
|
||||
"_hydrateIdLinkedHistoricalToolScenes(prev, {{sessionId:'sid-6220', mode:'compact_worklog'}})",
|
||||
sandbox
|
||||
);
|
||||
const next = JSON.parse(JSON.stringify(original));
|
||||
sandbox.next = next;
|
||||
vm.runInContext("_carryForwardEphemeralTurnFields(prev, next)", sandbox);
|
||||
const carried = next
|
||||
.map((message, index) => message && message._anchor_activity_scene ? {{index, turn_id:message._anchor_activity_scene.identity.turn_id, tools:message._anchor_activity_scene.activity_rows.map(row => row.tool_call_id)}} : null)
|
||||
.filter(Boolean);
|
||||
const hydrated = vm.runInContext(
|
||||
"_hydrateIdLinkedHistoricalToolScenes(next, {{sessionId:'sid-6220', mode:'compact_worklog'}})",
|
||||
sandbox
|
||||
);
|
||||
const owners = next
|
||||
.map((message, index) => message && message._anchor_activity_scene ? {{index, turn_id:message._anchor_activity_scene.identity.turn_id, tools:message._anchor_activity_scene.activity_rows.map(row => row.tool_call_id)}} : null)
|
||||
.filter(Boolean);
|
||||
console.log(JSON.stringify({{carried, hydrated, owners}}));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[NODE, "-e", script],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def _linked_turn(call_id: str = "call-1") -> list[dict]:
|
||||
return [
|
||||
{"role": "user", "content": "Inspect the workspace"},
|
||||
@@ -188,6 +295,32 @@ def test_id_linked_historical_tool_turn_hydrates_one_anchor_scene_idempotently()
|
||||
assert row["tool"]["done"] is True
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for Anchor hydration tests")
|
||||
def test_id_linked_historical_final_answer_preserves_raw_string_whitespace():
|
||||
messages = _linked_turn()
|
||||
messages[-1]["content"] = " indented code\n"
|
||||
|
||||
data = _run_hydration(messages)
|
||||
|
||||
assert data["first"] == 1
|
||||
assert data["owners"][0]["scene"]["final_answer"] == " indented code\n"
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for Anchor hydration tests")
|
||||
def test_id_linked_historical_final_answer_joins_text_parts_like_renderer():
|
||||
messages = _linked_turn()
|
||||
messages[-1]["content"] = [
|
||||
{"type": "text", "text": "First paragraph."},
|
||||
{"type": "input_text", "text": "ignored input"},
|
||||
{"type": "text", "text": "Second paragraph."},
|
||||
]
|
||||
|
||||
data = _run_hydration(messages)
|
||||
|
||||
assert data["first"] == 1
|
||||
assert data["owners"][0]["scene"]["final_answer"] == "First paragraph.\nSecond paragraph."
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for Anchor hydration tests")
|
||||
def test_id_linked_historical_multi_tool_turn_preserves_declaration_order():
|
||||
messages = [
|
||||
@@ -441,6 +574,26 @@ def test_reused_ids_in_adjacent_complete_turns_remain_turn_scoped():
|
||||
assert data["owners"][0]["scene"]["identity"]["turn_id"] != data["owners"][1]["scene"]["identity"]["turn_id"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for Anchor hydration tests")
|
||||
def test_duplicate_final_answers_rebuild_historical_scenes_after_refresh():
|
||||
first_turn = _linked_turn("call-first")
|
||||
second_turn = _linked_turn("call-second")
|
||||
first_turn[1]["tool_calls"][0]["function"]["name"] = "first_tool"
|
||||
first_turn[2]["content"] = "first result"
|
||||
first_turn[3]["content"] = "Done"
|
||||
second_turn[1]["tool_calls"][0]["function"]["name"] = "second_tool"
|
||||
second_turn[2]["content"] = "second result"
|
||||
second_turn[3]["content"] = "Done"
|
||||
|
||||
data = _run_carry_forward_then_hydration(first_turn + second_turn)
|
||||
|
||||
assert data["carried"] == []
|
||||
assert data["hydrated"] == 2
|
||||
assert [owner["index"] for owner in data["owners"]] == [3, 7]
|
||||
assert [owner["tools"] for owner in data["owners"]] == [["call-first"], ["call-second"]]
|
||||
assert data["owners"][0]["turn_id"] != data["owners"][1]["turn_id"]
|
||||
|
||||
|
||||
def test_render_messages_hydrates_before_visibility_and_fallback_ownership_scan():
|
||||
start = UI_JS.index("function renderMessages(options)")
|
||||
body = UI_JS[start : UI_JS.index("\nfunction ", start + 1)]
|
||||
|
||||
Reference in new issue
Block a user