Merge pull request #6222 from franksong2702/franksong2702/live-stream-id-linked-transcript-anchor
Live Stream: hydrate ID-linked historical tools into Anchors
This commit is contained in:
6 files changed
+889
-1
No files matched your search
@@ -317,7 +317,7 @@ must not compete with an anchor-owned turn.
|
||||
| --- | --- | --- | --- |
|
||||
| Settled Compact Worklog activity | `_renderSettledAnchorSceneForMessage()` renders `activity_scene_v1` rows before the final answer. | Legacy `S.toolCalls`, `tool_calls`, `_partial_tool_calls`, and raw `content[].tool_use` rebuilds. | `anchorOwnedAssistantRawIdxs` excludes the anchor-owned turn from message metadata scans, fallback source collection, tool buckets, thinking buckets, and worklog-source mirroring. |
|
||||
| Settled Transparent Stream activity | `_renderSettledAnchorSceneTransparentForMessage()` renders the same scene rows as transparent event rows or inline prose before the final answer segment. | `_transparentStreamOrderedParts()` rebuilds raw `content[]` order for historical messages without an anchor scene. | `_transparentStreamOrderedParts()` returns `null` when `message._anchor_activity_scene` exists, leaving the dedicated anchor renderer in charge. |
|
||||
| Historical / non-anchor transcripts | No anchor owner is available. | Raw `content[]`, persisted `session.tool_calls`, role=`tool` rows, and partial tool-call snapshots continue to recover visible tool/prose history. | None. These paths remain until replay/runtime-journal coverage proves the same transcript shapes hydrate through anchors. |
|
||||
| Historical / non-anchor transcripts | Complete, user-bounded transcripts with unique top-level `assistant.tool_calls[].id` declarations, exactly one matching role=`tool` `tool_call_id` result per declaration, and a later final assistant answer hydrate an in-memory `activity_scene_v1` through the Anchor module adapter before settled rendering. | Raw `content[]`, persisted `session.tool_calls`, role=`tool` rows, and partial tool-call snapshots continue to recover ambiguous, incomplete, or richer historical shapes. | The ID-linked shape exits fallback ownership only after strict turn-local validation; missing or duplicate IDs/results, cross-turn matches, process prose/reasoning, and other tool schemas fail closed to the legacy renderer. |
|
||||
| Live reattach / session switch | `renderLiveAnchorActivityScene()` consumes the projected live scene, and session switch first attempts runtime-journal anchor scene restore. | Saved live DOM snapshots and `INFLIGHT` tool replay. | Snapshot fallback only runs when no usable live anchor scene can be rendered. |
|
||||
|
||||
This matrix is the current settled-render contract, backed by runtime-journal
|
||||
|
||||
@@ -928,6 +928,7 @@
|
||||
);
|
||||
const done=_activityRowToolDone(kind,status,payload);
|
||||
const isError=_activityRowToolIsError(status,payload);
|
||||
const isDiff=_own(payload,'is_diff')===true||_own(payload,'isDiff')===true;
|
||||
const signatureParts=[
|
||||
toolName,
|
||||
toolCallId||'',
|
||||
@@ -943,6 +944,7 @@
|
||||
output:_sanitizePayload(_own(payload,'output'))??null,
|
||||
done,
|
||||
is_error:isError,
|
||||
...(isDiff?{is_diff:true}:{}),
|
||||
duration:_activityPayloadFirst(payload,['duration','duration_seconds','elapsed'])??null,
|
||||
started_at:_activityPayloadFirst(payload,['started_at','startedAt'])??null,
|
||||
signature:signatureParts.join('|'),
|
||||
@@ -1058,6 +1060,51 @@
|
||||
});
|
||||
}
|
||||
|
||||
function projectAssistantTurnAnchorHistoricalTranscriptScene(input, options){
|
||||
const item=(input&&typeof input==='object')?input:{};
|
||||
const sessionId=_cleanString(_own(item,'session_id'));
|
||||
const turnId=_cleanString(_own(item,'turn_id'));
|
||||
const localId=_cleanString(_own(item,'local_id'));
|
||||
const activityEvents=Array.isArray(_own(item,'activity_events'))?_own(item,'activity_events'):[];
|
||||
const settledMessage=_own(item,'settled_message');
|
||||
if(!sessionId||!turnId||!localId||!activityEvents.length
|
||||
||!settledMessage||typeof settledMessage!=='object') return null;
|
||||
const runId=_cleanString(_own(item,'run_id'))||null;
|
||||
const streamId=_cleanString(_own(item,'stream_id'))||null;
|
||||
const sourceMessageRefs=Array.isArray(_own(item,'source_message_refs'))
|
||||
?_own(item,'source_message_refs').map(_cleanString).filter(Boolean)
|
||||
:[];
|
||||
const registry=createAssistantTurnAnchorRegistry({
|
||||
session_id:sessionId,
|
||||
turn_id:turnId,
|
||||
run_id:runId,
|
||||
stream_id:streamId,
|
||||
local_id:localId,
|
||||
source_message_refs:sourceMessageRefs,
|
||||
});
|
||||
const context={
|
||||
session_id:sessionId,
|
||||
turn_id:turnId,
|
||||
run_id:runId,
|
||||
stream_id:streamId,
|
||||
};
|
||||
for(const event of activityEvents){
|
||||
const result=applyAssistantTurnAnchorSourceEvent(registry,event,context);
|
||||
if(!result.applied||result.normalized.classification!=='activity') return null;
|
||||
}
|
||||
const settledPayload={...settledMessage,role:_cleanString(_own(settledMessage,'role'))||'assistant'};
|
||||
const settled=applyAssistantTurnAnchorSourceEvent(registry,{
|
||||
source_type:'settled_message',
|
||||
seq:activityEvents.length+1,
|
||||
local_id:localId,
|
||||
payload:settledPayload,
|
||||
},context);
|
||||
if(!settled.applied) return null;
|
||||
const opts=(options&&typeof options==='object')?options:{};
|
||||
const requestedMode=_cleanString(_own(opts,'mode'))||_cleanString(_own(item,'mode'));
|
||||
return projectAssistantTurnAnchorActivityScene(registry,{mode:requestedMode});
|
||||
}
|
||||
|
||||
const ACTIVITY_RECONCILIATION_DEFAULT_FIELDS=Object.freeze([
|
||||
'kind',
|
||||
'role',
|
||||
@@ -1658,6 +1705,7 @@
|
||||
createAssistantTurnAnchorShadowSnapshot,
|
||||
projectAssistantTurnAnchorSettledMessageFinalAnswer,
|
||||
projectAssistantTurnAnchorActivityScene,
|
||||
projectAssistantTurnAnchorHistoricalTranscriptScene,
|
||||
reconcileAssistantTurnAnchorActivityScene,
|
||||
createAssistantTurnAnchorRendererSnapshot,
|
||||
reconcileAssistantTurnAnchorRendererSnapshot,
|
||||
|
||||
@@ -6504,6 +6504,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;
|
||||
@@ -6518,6 +6524,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];
|
||||
}
|
||||
}
|
||||
|
||||
+225
@@ -12309,6 +12309,7 @@ function _anchorSceneToolCallFromRow(row, opts){
|
||||
)||'',
|
||||
done:settled?true:(tool.done!==null&&tool.done!==undefined?tool.done:(row.status!=='running'&&row.status!=='pending')),
|
||||
is_error:!!(tool.is_error||payload.is_error||row.status==='error'||row.status==='failed'),
|
||||
is_diff:!!(tool.is_diff||payload.is_diff||payload.isDiff),
|
||||
duration:tool.duration||payload.duration||payload.duration_seconds,
|
||||
started_at:firstValidTimestampSeconds(tool.started_at, payload.started_at, rowTs),
|
||||
created_at:firstValidTimestampSeconds(tool.created_at, payload.created_at, rowTs),
|
||||
@@ -14706,6 +14707,226 @@ function _toolArgsSnapshot(args, limit){
|
||||
return out;
|
||||
}
|
||||
|
||||
function _idLinkedHistoricalMessageText(message){
|
||||
if(!message||typeof message!=='object') return '';
|
||||
const content=message.content;
|
||||
if(typeof content==='string') return content;
|
||||
if(!Array.isArray(content)) return '';
|
||||
return content.filter(part=>part&&typeof part==='object'&&part.type==='text').map(part=>{
|
||||
if(!part||typeof part!=='object') return '';
|
||||
return String(part.text||part.content||'');
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
function _idLinkedHistoricalMessageHasVisibleText(message){
|
||||
return _idLinkedHistoricalMessageText(message).trim()!=='';
|
||||
}
|
||||
|
||||
function _idLinkedHistoricalMessageRef(message, rawIdx){
|
||||
if(message&&typeof message==='object'){
|
||||
for(const key of ['message_id','id','local_id']){
|
||||
const value=message[key];
|
||||
if(typeof value==='string'&&value.trim()) return value.trim();
|
||||
if(typeof value==='number'&&Number.isFinite(value)) return String(value);
|
||||
}
|
||||
}
|
||||
return `raw_idx:${rawIdx}`;
|
||||
}
|
||||
|
||||
function _idLinkedHistoricalToolArguments(toolCall){
|
||||
if(!toolCall||typeof toolCall!=='object') return null;
|
||||
const fn=toolCall.function;
|
||||
if(!fn||typeof fn!=='object'||Array.isArray(fn)) return null;
|
||||
const raw=fn.arguments;
|
||||
if(raw===undefined||raw===null||raw==='') return null;
|
||||
if(raw&&typeof raw==='object'&&!Array.isArray(raw)) return raw;
|
||||
if(typeof raw!=='string') return null;
|
||||
try{
|
||||
const parsed=JSON.parse(raw);
|
||||
return parsed&&typeof parsed==='object'&&!Array.isArray(parsed)?parsed:null;
|
||||
}catch(e){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function _idLinkedHistoricalToolResultRaw(message){
|
||||
if(!message||typeof message!=='object') return null;
|
||||
const content=message.content;
|
||||
return typeof content==='string'?content:null;
|
||||
}
|
||||
|
||||
function _idLinkedHistoricalRedactSnippet(value){
|
||||
let text=String(value||'');
|
||||
if(!text) return '';
|
||||
if(typeof _redactToolTargetLabel==='function'){
|
||||
try{text=_redactToolTargetLabel(text);}
|
||||
catch(e){}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function _idLinkedHistoricalHasVisibleSidecar(message){
|
||||
if(!message||typeof message!=='object') return false;
|
||||
const visibleKeys=['attachments','_attachments','_statusCard','status_card','statusCard','card','cards','artifact','artifacts','files','images','media'];
|
||||
for(const key of visibleKeys){
|
||||
if(!Object.prototype.hasOwnProperty.call(message,key)) continue;
|
||||
const value=message[key];
|
||||
if(value===undefined||value===null||value===false) continue;
|
||||
if(Array.isArray(value)&&value.length===0) continue;
|
||||
if(typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).length===0) continue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Claim legacy settled ownership only when the transcript itself proves a
|
||||
// complete, user-bounded declaration/result/final-answer chain.
|
||||
function _idLinkedHistoricalTurnScene(messages, turnStart, turnEnd, options){
|
||||
const list=Array.isArray(messages)?messages:[];
|
||||
const start=Math.max(0,Number(turnStart)||0);
|
||||
const end=Math.min(list.length,Math.max(start,Number(turnEnd)||0));
|
||||
const opts=options&&typeof options==='object'?options:{};
|
||||
const sessionId=String(opts.sessionId||opts.session_id||'').trim();
|
||||
const api=(typeof window!=='undefined')?window.HermesAssistantTurnAnchors:null;
|
||||
if(!sessionId||!api||typeof api.projectAssistantTurnAnchorHistoricalTranscriptScene!=='function') return null;
|
||||
|
||||
const declarations=[];
|
||||
const declarationIds=new Set();
|
||||
const declarationRefs=[];
|
||||
const visibleAssistantIndexes=[];
|
||||
const assistantIndexes=[];
|
||||
const resultsById=new Map();
|
||||
for(let rawIdx=start;rawIdx<end;rawIdx++){
|
||||
const message=list[rawIdx];
|
||||
if(!message||typeof message!=='object') continue;
|
||||
const role=message.role;
|
||||
if(role==='user'&&rawIdx===start) continue;
|
||||
if(message._anchor_activity_scene) return null;
|
||||
if(role==='assistant'){
|
||||
assistantIndexes.push(rawIdx);
|
||||
const hasVisibleText=_idLinkedHistoricalMessageHasVisibleText(message);
|
||||
const reasoningText=_assistantReasoningPayloadText(message);
|
||||
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&&hasVisibleText) return null;
|
||||
if(!toolCalls.length){
|
||||
if(hasVisibleText) continue;
|
||||
return null;
|
||||
}
|
||||
if(message.content!==undefined&&message.content!==null&&message.content!=='') return null;
|
||||
const messageRef=_idLinkedHistoricalMessageRef(message,rawIdx);
|
||||
if(!declarationRefs.includes(messageRef)) declarationRefs.push(messageRef);
|
||||
for(const toolCall of toolCalls){
|
||||
const callId=String(toolCall&&toolCall.id||'').trim();
|
||||
const fn=toolCall&&toolCall.function;
|
||||
const name=String(fn&&fn.name||'').trim();
|
||||
const args=_idLinkedHistoricalToolArguments(toolCall);
|
||||
if(!callId||!name||args===null||declarationIds.has(callId)) return null;
|
||||
declarationIds.add(callId);
|
||||
declarations.push({callId,name,args,rawIdx,messageRef});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if(role!=='tool') return null;
|
||||
const callId=String(message.tool_call_id||'').trim();
|
||||
if(!callId||!declarationIds.has(callId)) return null;
|
||||
const matches=resultsById.get(callId)||[];
|
||||
matches.push({message,rawIdx});
|
||||
resultsById.set(callId,matches);
|
||||
}
|
||||
|
||||
if(!declarations.length||visibleAssistantIndexes.length!==1) return null;
|
||||
const ownerIndex=visibleAssistantIndexes[0];
|
||||
if(ownerIndex!==assistantIndexes[assistantIndexes.length-1]) return null;
|
||||
const owner=list[ownerIndex];
|
||||
if(Array.isArray(owner.tool_calls)&&owner.tool_calls.length) return null;
|
||||
const ownerRef=_idLinkedHistoricalMessageRef(owner,ownerIndex);
|
||||
for(const declaration of declarations){
|
||||
const matches=resultsById.get(declaration.callId)||[];
|
||||
if(matches.length!==1||matches[0].rawIdx<=declaration.rawIdx||matches[0].rawIdx>=ownerIndex) return null;
|
||||
}
|
||||
if(resultsById.size!==declarations.length) return null;
|
||||
|
||||
const sourceRefs=declarationRefs.concat(ownerRef).filter((value,index,array)=>array.indexOf(value)===index);
|
||||
const turnId=['historical',sessionId,declarationRefs[0],ownerRef].join(':');
|
||||
const activityEvents=[];
|
||||
for(let index=0;index<declarations.length;index++){
|
||||
const declaration=declarations[index];
|
||||
const resultEntry=resultsById.get(declaration.callId)[0];
|
||||
const args=_toolArgsSnapshot(declaration.args);
|
||||
const resultRaw=_idLinkedHistoricalToolResultRaw(resultEntry.message);
|
||||
if(resultRaw===null) return null;
|
||||
const resultSnippet=_idLinkedHistoricalRedactSnippet(_cliToolResultSnippet(resultRaw));
|
||||
const patchSnippet=_cliPatchSnippetFromArgs(declaration.name,args);
|
||||
const isDiff=_cliToolCardHasDiffSnippet(resultSnippet,patchSnippet);
|
||||
const snippet=_idLinkedHistoricalRedactSnippet(_cliToolCardSnippet(resultSnippet,patchSnippet));
|
||||
const status=String(resultEntry.message.status||'').trim().toLowerCase();
|
||||
const isError=resultEntry.message.is_error===true||status==='error'||status==='failed'||status==='failure';
|
||||
activityEvents.push({
|
||||
source_type:'tool_complete',
|
||||
seq:index+1,
|
||||
local_id:`historical:${declaration.messageRef}:tool:${declaration.callId}`,
|
||||
payload:{
|
||||
id:declaration.callId,
|
||||
tid:declaration.callId,
|
||||
tool_call_id:declaration.callId,
|
||||
name:declaration.name,
|
||||
args,
|
||||
command:String(args.command||args.cmd||''),
|
||||
snippet,
|
||||
done:true,
|
||||
is_error:isError,
|
||||
is_diff:isDiff,
|
||||
assistant_msg_idx:declaration.rawIdx,
|
||||
},
|
||||
});
|
||||
}
|
||||
let scene;
|
||||
try{
|
||||
scene=api.projectAssistantTurnAnchorHistoricalTranscriptScene({
|
||||
session_id:sessionId,
|
||||
turn_id:turnId,
|
||||
local_id:ownerRef,
|
||||
source_message_refs:sourceRefs,
|
||||
activity_events:activityEvents,
|
||||
settled_message:{role:'assistant',id:ownerRef,content:_idLinkedHistoricalMessageText(owner)},
|
||||
},{mode:opts.mode||'compact_worklog'});
|
||||
}catch(e){
|
||||
return null;
|
||||
}
|
||||
if(!scene||scene.version!=='activity_scene_v1'||scene.activity_rows.length!==declarations.length) return null;
|
||||
return {ownerIndex,scene};
|
||||
}
|
||||
|
||||
function _hydrateIdLinkedHistoricalToolScenes(messages, options){
|
||||
const list=Array.isArray(messages)?messages:[];
|
||||
let turnStart=-1;
|
||||
let hydrated=0;
|
||||
const hydrateTurn=(turnEnd)=>{
|
||||
if(turnStart<0||turnEnd<=turnStart+1) return;
|
||||
let hydratedTurn;
|
||||
try{hydratedTurn=_idLinkedHistoricalTurnScene(list,turnStart,turnEnd,options);}
|
||||
catch(e){return;}
|
||||
if(!hydratedTurn) return;
|
||||
const owner=list[hydratedTurn.ownerIndex];
|
||||
try{owner._anchor_activity_scene=hydratedTurn.scene;}
|
||||
catch(e){return;}
|
||||
if(owner._anchor_activity_scene===hydratedTurn.scene) hydrated+=1;
|
||||
};
|
||||
for(let rawIdx=0;rawIdx<list.length;rawIdx++){
|
||||
const message=list[rawIdx];
|
||||
if(!message||message.role!=='user') continue;
|
||||
hydrateTurn(rawIdx);
|
||||
turnStart=rawIdx;
|
||||
}
|
||||
hydrateTurn(list.length);
|
||||
return hydrated;
|
||||
}
|
||||
|
||||
function _captureMessageScrollSnapshot(){
|
||||
const el=$('messages');
|
||||
if(!el) return null;
|
||||
@@ -15349,6 +15570,10 @@ function renderMessages(options){
|
||||
const scrollSnapshot=(preserveScroll||_messageUserUnpinned)?_captureMessageScrollSnapshot():null;
|
||||
const inner=$('msgInner');
|
||||
const sid=S.session?S.session.session_id:null;
|
||||
if(!S.busy&&Array.isArray(S.messages)&&typeof _hydrateIdLinkedHistoricalToolScenes==='function'){
|
||||
const activityMode=typeof chatActivityMode==='function'?chatActivityMode():'compact_worklog';
|
||||
_hydrateIdLinkedHistoricalToolScenes(S.messages,{sessionId:sid,mode:activityMode});
|
||||
}
|
||||
const msgCount=S.messages.length;
|
||||
// During session switch, S.messages is intentionally cleared while the full
|
||||
// message fetch is still in flight. Other async updates can still call
|
||||
|
||||
@@ -61,6 +61,7 @@ function installRuntimeHelpers() {
|
||||
"_filterRecoveryControlMessages",
|
||||
"_replaceMarkerOnlyAssistantWithStreamError",
|
||||
"_messageIdentityKey",
|
||||
"_isHistoricalAnchorActivityScene",
|
||||
"_carryForwardEphemeralTurnFields",
|
||||
"_isTerminalStreamErrorMarkerMessage",
|
||||
"_ensureSingleTerminalStreamErrorMarker",
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
"""Regression coverage for #6220 historical tool transcript ownership.
|
||||
|
||||
Only an unambiguous ``assistant.tool_calls[].id`` ->
|
||||
``role=tool.tool_call_id`` turn may move from the legacy settled renderer into
|
||||
an Assistant Turn Anchor scene. Ambiguous or richer historical shapes stay on
|
||||
the compatibility path.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
ANCHOR_JS = ROOT / "static" / "assistant_turn_anchors.js"
|
||||
NODE = shutil.which("node")
|
||||
|
||||
|
||||
_EXTRACT_FUNC_JS = r"""
|
||||
function extractFunc(name){
|
||||
const start = src.indexOf('function ' + name);
|
||||
if(start === -1) throw new Error(name + ' not found');
|
||||
const params = src.indexOf('(', start);
|
||||
let depth = 0, close = -1;
|
||||
for(let i=params; i<src.length; i++){
|
||||
if(src[i] === '(') depth++;
|
||||
else if(src[i] === ')'){
|
||||
depth--;
|
||||
if(depth === 0){ close = i; break; }
|
||||
}
|
||||
}
|
||||
const brace = src.indexOf('{', close);
|
||||
depth = 0;
|
||||
for(let i=brace; i<src.length; i++){
|
||||
if(src[i] === '{') depth++;
|
||||
else if(src[i] === '}'){
|
||||
depth--;
|
||||
if(depth === 0) return src.slice(start, i + 1);
|
||||
}
|
||||
}
|
||||
throw new Error(name + ' body did not close');
|
||||
}
|
||||
""".strip()
|
||||
|
||||
|
||||
def _run_hydration(messages: list[dict], *, repeat: bool = False, throw_helper: bool = False) -> dict:
|
||||
assert NODE, "node is required for Anchor hydration behavior tests"
|
||||
script = f"""
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
const src = fs.readFileSync({json.dumps(str(ROOT / 'static' / 'ui.js'))}, 'utf8');
|
||||
{_EXTRACT_FUNC_JS}
|
||||
|
||||
const sandbox = {{console}};
|
||||
sandbox.window = sandbox;
|
||||
sandbox.globalThis = sandbox;
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(
|
||||
fs.readFileSync({json.dumps(str(ANCHOR_JS))}, 'utf8'),
|
||||
sandbox,
|
||||
{{filename:'assistant_turn_anchors.js'}}
|
||||
);
|
||||
sandbox._redactToolTargetLabel = value => String(value)
|
||||
.replace(/([A-Za-z0-9_]*(?:TOKEN|API[_-]?KEY|SECRET|PASSWORD)[A-Za-z0-9_]*\\s*=\\s*)(?:"[^"]*"|'[^']*'|\\S+)/gi, '$1[redacted]');
|
||||
|
||||
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 = src.indexOf('function ' + name) >= 0 ? extractFunc(name) : null;
|
||||
if(fn) vm.runInContext(fn, sandbox, {{filename:'ui.js:' + name}});
|
||||
}}
|
||||
|
||||
if({str(throw_helper).lower()}){{
|
||||
vm.runInContext(
|
||||
"_toolArgsSnapshot = function(){{ throw new Error('synthetic event build failure'); }}",
|
||||
sandbox
|
||||
);
|
||||
}}
|
||||
|
||||
const messages = {json.dumps(messages)};
|
||||
sandbox.messages = messages;
|
||||
const first = vm.runInContext(
|
||||
"_hydrateIdLinkedHistoricalToolScenes(messages, {{sessionId:'sid-6220', mode:'compact_worklog'}})",
|
||||
sandbox
|
||||
);
|
||||
const afterFirst = JSON.stringify(messages);
|
||||
const second = {str(repeat).lower()}
|
||||
? vm.runInContext(
|
||||
"_hydrateIdLinkedHistoricalToolScenes(messages, {{sessionId:'sid-6220', mode:'compact_worklog'}})",
|
||||
sandbox
|
||||
)
|
||||
: null;
|
||||
const owners = messages
|
||||
.map((message, index) => message && message._anchor_activity_scene ? {{index, scene:message._anchor_activity_scene}} : null)
|
||||
.filter(Boolean);
|
||||
sandbox.owners = owners;
|
||||
const toolCards = vm.runInContext(
|
||||
"owners.map(owner => owner.scene.activity_rows.map(row => _anchorSceneToolCallFromRow(row, {{settled:true}})))",
|
||||
sandbox
|
||||
);
|
||||
console.log(JSON.stringify({{first, second, unchanged:afterFirst===JSON.stringify(messages), owners, toolCards}}));
|
||||
"""
|
||||
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 _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"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": '{"command":"pwd"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": call_id,
|
||||
"content": '{"output":"/workspace"}',
|
||||
},
|
||||
{"role": "assistant", "content": "The workspace is `/workspace`."},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for Anchor hydration tests")
|
||||
def test_id_linked_historical_tool_turn_hydrates_one_anchor_scene_idempotently():
|
||||
data = _run_hydration(_linked_turn(), repeat=True)
|
||||
|
||||
assert data["first"] == 1
|
||||
assert data["second"] == 0
|
||||
assert data["unchanged"] is True
|
||||
assert [owner["index"] for owner in data["owners"]] == [3]
|
||||
|
||||
scene = data["owners"][0]["scene"]
|
||||
assert scene["version"] == "activity_scene_v1"
|
||||
assert scene["final_answer"] == "The workspace is `/workspace`."
|
||||
assert scene["final_message_ref"] == "raw_idx:3"
|
||||
assert scene["identity"]["session_id"] == "sid-6220"
|
||||
assert scene["identity"]["source_message_refs"] == ["raw_idx:1", "raw_idx:3"]
|
||||
assert len(scene["activity_rows"]) == 1
|
||||
|
||||
row = scene["activity_rows"][0]
|
||||
assert row["role"] == "tool"
|
||||
assert row["source_event_type"] == "tool_complete"
|
||||
assert row["tool_call_id"] == "call-1"
|
||||
assert row["tool"]["name"] == "terminal"
|
||||
assert row["tool"]["args"] == {"command": "pwd"}
|
||||
assert row["tool"]["snippet"] == "/workspace"
|
||||
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 = [
|
||||
{"role": "user", "content": "Inspect both files"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call-first",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": '{"command":"pwd"}',
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "call-second",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"arguments": '{"path":"README.md"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call-second", "content": "readme body"},
|
||||
{"role": "tool", "tool_call_id": "call-first", "content": "/workspace"},
|
||||
{"role": "assistant", "content": "Both checks completed."},
|
||||
]
|
||||
|
||||
data = _run_hydration(messages, repeat=True)
|
||||
|
||||
assert data["first"] == 1
|
||||
assert data["second"] == 0
|
||||
rows = data["owners"][0]["scene"]["activity_rows"]
|
||||
assert [row["tool_call_id"] for row in rows] == ["call-first", "call-second"]
|
||||
assert [row["tool"]["name"] for row in rows] == ["terminal", "read_file"]
|
||||
assert [row["tool"]["snippet"] for row in rows] == ["/workspace", "readme body"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for Anchor hydration tests")
|
||||
@pytest.mark.parametrize(
|
||||
"messages",
|
||||
[
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"function": {"name": "terminal", "arguments": "{}"}}]},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [
|
||||
{"id": "call-1", "function": {"name": "one", "arguments": "{}"}},
|
||||
{"id": "call-1", "function": {"name": "two", "arguments": "{}"}},
|
||||
]},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{}"}}]},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{}"}}]},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "first"},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "second"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": "I will inspect it first.", "tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{}"}}]},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": None, "reasoning": "Need to inspect it.", "tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{}"}}]},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{}"}}]},
|
||||
{"role": "user", "content": "What happened?"},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": [{"type": "tool_use", "id": "tool-1", "name": "terminal", "input": {}}]},
|
||||
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "tool-1", "content": "ok"}]},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{not-json"}}]},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{}"}}],
|
||||
"_partial_tool_calls": [{"name": "terminal"}],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
],
|
||||
)
|
||||
def test_ambiguous_or_richer_historical_tool_shapes_keep_legacy_ownership(messages):
|
||||
data = _run_hydration(messages)
|
||||
|
||||
assert data["first"] == 0
|
||||
assert data["owners"] == []
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for Anchor hydration tests")
|
||||
@pytest.mark.parametrize("content", [
|
||||
{"stdout": "...", "env": "API_KEY=result-secret-123"},
|
||||
[{"stdout": "ok"}],
|
||||
None,
|
||||
])
|
||||
def test_non_string_historical_tool_results_keep_legacy_ownership(content):
|
||||
messages = _linked_turn()
|
||||
messages[2]["content"] = content
|
||||
|
||||
data = _run_hydration(messages)
|
||||
|
||||
assert data["first"] == 0
|
||||
assert data["owners"] == []
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for Anchor hydration tests")
|
||||
def test_string_historical_tool_result_is_redacted_before_anchor_scene():
|
||||
messages = _linked_turn()
|
||||
messages[2]["content"] = '{"output":"API_KEY=result-secret-123"}'
|
||||
|
||||
data = _run_hydration(messages)
|
||||
|
||||
assert data["first"] == 1
|
||||
row = data["owners"][0]["scene"]["activity_rows"][0]
|
||||
assert "result-secret-123" not in json.dumps(row)
|
||||
assert "API_KEY=[redacted]" in row["tool"]["snippet"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for Anchor hydration tests")
|
||||
@pytest.mark.parametrize(
|
||||
"messages",
|
||||
[
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{}"}}]},
|
||||
{"role": "system", "content": "visible system status"},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{}"}}]},
|
||||
{"role": "developer", "content": "visible developer note"},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{}"}}]},
|
||||
{"role": "function", "name": "legacy_func", "content": "legacy function output"},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{}"}}]},
|
||||
{"role": "assistant", "content": None, "tool_calls": []},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"attachments": [{"name": "evidence.txt"}],
|
||||
"tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{}"}}],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"_statusCard": {"title": "Visible status"},
|
||||
"tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": "{}"}}],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
[
|
||||
{"role": "user", "content": "Run it"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"id": "call-1", "function": {"name": "terminal", "arguments": ""}}]},
|
||||
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
|
||||
{"role": "assistant", "content": "Done"},
|
||||
],
|
||||
],
|
||||
)
|
||||
def test_non_whitelisted_intermediate_records_keep_legacy_ownership(messages):
|
||||
data = _run_hydration(messages)
|
||||
|
||||
assert data["first"] == 0
|
||||
assert data["owners"] == []
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for Anchor hydration tests")
|
||||
def test_diff_historical_tool_result_preserves_diff_card_projection():
|
||||
messages = _linked_turn()
|
||||
messages[2]["content"] = "diff --git a/demo.txt b/demo.txt\n@@\n-old\n+new\n"
|
||||
|
||||
data = _run_hydration(messages)
|
||||
|
||||
assert data["first"] == 1
|
||||
row = data["owners"][0]["scene"]["activity_rows"][0]
|
||||
assert row["payload"]["is_diff"] is True
|
||||
assert row["tool"]["is_diff"] is True
|
||||
assert data["toolCards"][0][0]["is_diff"] is True
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for Anchor hydration tests")
|
||||
def test_event_building_helper_exceptions_keep_legacy_ownership():
|
||||
data = _run_hydration(_linked_turn(), throw_helper=True)
|
||||
|
||||
assert data["first"] == 0
|
||||
assert data["owners"] == []
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for Anchor hydration tests")
|
||||
def test_reused_ids_in_adjacent_complete_turns_remain_turn_scoped():
|
||||
data = _run_hydration(_linked_turn("call-reused") + _linked_turn("call-reused"))
|
||||
|
||||
assert data["first"] == 2
|
||||
assert [owner["index"] for owner in data["owners"]] == [3, 7]
|
||||
assert [owner["scene"]["activity_rows"][0]["tool_call_id"] for owner in data["owners"]] == ["call-reused", "call-reused"]
|
||||
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)]
|
||||
|
||||
hydrate = "_hydrateIdLinkedHistoricalToolScenes(S.messages"
|
||||
visible = "const visWithIdx=_getVisibleMessagesWithIdx();"
|
||||
fallback = "const anchorOwnedAssistantRawIdxs=new Set();"
|
||||
|
||||
assert hydrate in body
|
||||
assert body.index(hydrate) < body.index(visible) < body.index(fallback)
|
||||
assert "if(!S.busy" in body[: body.index(visible)]
|
||||
Reference in new issue
Block a user