feat(anchor): project activity scene rows
This commit is contained in:
@@ -23,10 +23,13 @@ streaming or rendering yet.
|
|||||||
pinned by tests before visible wiring begins.
|
pinned by tests before visible wiring begins.
|
||||||
- Slice 4 starts RFC Phase 3 by routing settled assistant final prose through the
|
- Slice 4 starts RFC Phase 3 by routing settled assistant final prose through the
|
||||||
anchor owner before `renderMessages()` renders the final assistant body.
|
anchor owner before `renderMessages()` renders the final assistant body.
|
||||||
- The next independently reviewable boundary is activity/render-scene projection
|
- Slice 5 starts RFC Phase 5 by projecting anchor-owned activity events into a
|
||||||
for reasoning, tool rows, and transparent-stream/worklog metadata. `S.messages`,
|
renderer-neutral activity scene that Compact Worklog and Transparent Stream
|
||||||
`INFLIGHT`, stream-local state, and DOM nodes remain projection/cache layers
|
can later consume from the same ordered rows.
|
||||||
outside the settled final-prose path.
|
- The next independently reviewable boundary is wiring one current renderer to
|
||||||
|
the activity scene. `S.messages`, `INFLIGHT`, stream-local state, and DOM nodes
|
||||||
|
remain projection/cache layers outside the settled final-prose path and the
|
||||||
|
inert activity-scene projection.
|
||||||
|
|
||||||
## State Layers
|
## State Layers
|
||||||
|
|
||||||
@@ -136,6 +139,22 @@ This is intentionally narrower than render-scene ownership: live stream tokens,
|
|||||||
replay hydration, worklog rows, transparent-stream rows, tool cards, `INFLIGHT`,
|
replay hydration, worklog rows, transparent-stream rows, tool cards, `INFLIGHT`,
|
||||||
and DOM continuity are still not consumed by the anchor registry in this slice.
|
and DOM continuity are still not consumed by the anchor registry in this slice.
|
||||||
|
|
||||||
|
## Slice 5 Activity Scene Projection
|
||||||
|
|
||||||
|
`HermesAssistantTurnAnchors.projectAssistantTurnAnchorActivityScene()` projects
|
||||||
|
an anchor or registry into `activity_scene_v1`: identity, lifecycle,
|
||||||
|
`final_answer`, `final_message_ref`, terminal state, and an ordered
|
||||||
|
`activity_rows` list.
|
||||||
|
|
||||||
|
The rows are renderer-neutral. Compact Worklog receives display hints such as
|
||||||
|
`main_prose`, `collapsed_thinking`, `tool_row`, and `terminal_status_row`.
|
||||||
|
Transparent Stream receives the same row IDs, order, kinds, roles, text, tool
|
||||||
|
IDs, and sanitized payloads with a chronological display hint. This pins the
|
||||||
|
shared input shape before either renderer is rewired.
|
||||||
|
|
||||||
|
This slice is still inert. No current UI module consumes the activity scene.
|
||||||
|
`renderMessages()` and the live streaming hot path are unchanged by 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:
|
||||||
|
|||||||
@@ -786,6 +786,262 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _anchorFromProjectionInput(input){
|
||||||
|
if(!input||typeof input!=='object') return null;
|
||||||
|
if(input.anchor&&typeof input.anchor==='object') return input.anchor;
|
||||||
|
if(input.identity&&typeof input.identity==='object') return input;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activityRowId(event, index){
|
||||||
|
const eventId=_cleanString(_own(event,'event_id'));
|
||||||
|
if(eventId) return eventId;
|
||||||
|
const runId=_cleanString(_own(event,'run_id'));
|
||||||
|
const seq=_own(event,'seq');
|
||||||
|
if(runId&&seq!==undefined&&seq!==null&&seq!=='') return [runId,String(seq)].join(':');
|
||||||
|
const localId=_cleanString(_own(event,'local_id'));
|
||||||
|
if(localId){
|
||||||
|
const sourceType=_cleanString(_own(event,'source_event_type'))||_cleanString(_own(event,'kind'))||'event';
|
||||||
|
return [localId,sourceType,String(index)].join(':');
|
||||||
|
}
|
||||||
|
return 'activity:'+String(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activityRowText(event){
|
||||||
|
const payload=_own(event,'payload')||{};
|
||||||
|
return _firstTextValue(
|
||||||
|
_own(payload,'text'),
|
||||||
|
_own(payload,'content'),
|
||||||
|
_own(payload,'message'),
|
||||||
|
_own(payload,'summary'),
|
||||||
|
_own(payload,'result'),
|
||||||
|
_own(payload,'output')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isToolActivityKind(kind){
|
||||||
|
return kind==='tool_started'||kind==='tool_updated'||kind==='tool_completed';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activityRowToolId(event, kind){
|
||||||
|
if(!_isToolActivityKind(kind)) return null;
|
||||||
|
const payload=_own(event,'payload')||{};
|
||||||
|
return _firstTextValue(
|
||||||
|
_own(payload,'tool_call_id'),
|
||||||
|
_own(payload,'tool_use_id'),
|
||||||
|
_own(payload,'call_id'),
|
||||||
|
_own(payload,'tid'),
|
||||||
|
_own(payload,'id')
|
||||||
|
)||null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activityPayloadFirst(payload, keys){
|
||||||
|
return _firstOwn(payload||{},keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activityRowToolDone(kind, status, payload){
|
||||||
|
if(payload&&typeof _own(payload,'done')==='boolean') return _own(payload,'done');
|
||||||
|
if(kind==='tool_completed') return true;
|
||||||
|
if(kind==='tool_started'||kind==='tool_updated') return false;
|
||||||
|
if(status==='completed'||status==='error'||status==='failed') return true;
|
||||||
|
if(status==='running'||status==='pending') return false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activityRowToolIsError(status, payload){
|
||||||
|
if(payload&&typeof _own(payload,'is_error')==='boolean') return _own(payload,'is_error');
|
||||||
|
const raw=_cleanString(status).toLowerCase();
|
||||||
|
if(raw==='error'||raw==='failed'||raw==='failure') return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activityRowGroup(event, payload, index){
|
||||||
|
const activitySegmentSeq=_activityPayloadFirst(payload,['activitySegmentSeq','activity_segment_seq','segmentSeq','segment_seq']);
|
||||||
|
const activityBurstId=_activityPayloadFirst(payload,['activityBurstId','activity_burst_id','burstId','burst_id']);
|
||||||
|
const assistantMsgIdx=_activityPayloadFirst(payload,['assistant_msg_idx','assistantMessageIndex','assistant_msg_index']);
|
||||||
|
const cleanSegment=activitySegmentSeq!==undefined&&activitySegmentSeq!==null&&String(activitySegmentSeq)!==''
|
||||||
|
? activitySegmentSeq
|
||||||
|
: null;
|
||||||
|
const cleanBurst=activityBurstId!==undefined&&activityBurstId!==null&&String(activityBurstId)!==''
|
||||||
|
? activityBurstId
|
||||||
|
: null;
|
||||||
|
const cleanAssistant=assistantMsgIdx!==undefined&&assistantMsgIdx!==null&&String(assistantMsgIdx)!==''
|
||||||
|
? assistantMsgIdx
|
||||||
|
: null;
|
||||||
|
const fallbackSeq=_own(event,'seq');
|
||||||
|
const fallbackKey=fallbackSeq!==undefined&&fallbackSeq!==null&&fallbackSeq!==''?`event:${String(fallbackSeq)}`:`activity:${String(index)}`;
|
||||||
|
const groupKey=cleanSegment!==null
|
||||||
|
? `segment:${String(cleanSegment)}`
|
||||||
|
: cleanBurst!==null
|
||||||
|
? `burst:${String(cleanBurst)}`
|
||||||
|
: cleanAssistant!==null
|
||||||
|
? `assistant:${String(cleanAssistant)}`
|
||||||
|
: fallbackKey;
|
||||||
|
return Object.freeze({
|
||||||
|
group_key:groupKey,
|
||||||
|
activity_burst_id:cleanBurst,
|
||||||
|
activity_segment_seq:cleanSegment,
|
||||||
|
assistant_msg_idx:cleanAssistant,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activityRowThinking(event, kind, text){
|
||||||
|
if(kind!=='reasoning') return null;
|
||||||
|
const payload=_own(event,'payload')||{};
|
||||||
|
const thinkingText=_firstTextValue(
|
||||||
|
_own(payload,'thinking'),
|
||||||
|
_own(payload,'reasoning'),
|
||||||
|
_own(payload,'text'),
|
||||||
|
text
|
||||||
|
);
|
||||||
|
const preview=thinkingText?String(thinkingText).replace(/\s+/g,' ').trim():'';
|
||||||
|
return Object.freeze({
|
||||||
|
text:thinkingText||'',
|
||||||
|
preview:preview.length>180?`${preview.slice(0,177)}...`:preview,
|
||||||
|
dedupe_key:preview?`thinking:${preview.toLowerCase()}`:'',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activityRowTool(event, kind, status, text, toolCallId){
|
||||||
|
if(!_isToolActivityKind(kind)) return null;
|
||||||
|
const payload=_own(event,'payload')||{};
|
||||||
|
const toolName=_cleanString(
|
||||||
|
_activityPayloadFirst(payload,['name','tool_name','function_name'])||
|
||||||
|
(_own(payload,'function')&&_own(_own(payload,'function'),'name'))
|
||||||
|
)||'tool';
|
||||||
|
const args=_activityPayloadFirst(payload,['args','arguments','input','params']);
|
||||||
|
const preview=_firstTextValue(
|
||||||
|
_own(payload,'preview'),
|
||||||
|
_own(payload,'summary'),
|
||||||
|
text
|
||||||
|
);
|
||||||
|
const snippet=_firstTextValue(
|
||||||
|
_own(payload,'snippet'),
|
||||||
|
_own(payload,'result'),
|
||||||
|
_own(payload,'output')
|
||||||
|
);
|
||||||
|
const done=_activityRowToolDone(kind,status,payload);
|
||||||
|
const isError=_activityRowToolIsError(status,payload);
|
||||||
|
const signatureParts=[
|
||||||
|
toolName,
|
||||||
|
toolCallId||'',
|
||||||
|
JSON.stringify(_sanitizePayload(args||{})),
|
||||||
|
];
|
||||||
|
return Object.freeze({
|
||||||
|
id:toolCallId,
|
||||||
|
name:toolName,
|
||||||
|
args:_sanitizePayload(args||{}),
|
||||||
|
preview:preview||'',
|
||||||
|
snippet:snippet||'',
|
||||||
|
result:_sanitizePayload(_own(payload,'result'))??null,
|
||||||
|
output:_sanitizePayload(_own(payload,'output'))??null,
|
||||||
|
done,
|
||||||
|
is_error:isError,
|
||||||
|
duration:_activityPayloadFirst(payload,['duration','duration_seconds','elapsed'])??null,
|
||||||
|
started_at:_activityPayloadFirst(payload,['started_at','startedAt'])??null,
|
||||||
|
signature:signatureParts.join('|'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activityRowRole(kind){
|
||||||
|
if(kind==='process_prose') return 'prose';
|
||||||
|
if(kind==='reasoning') return 'thinking';
|
||||||
|
if(_isToolActivityKind(kind)) return 'tool';
|
||||||
|
if(kind==='lifecycle_status') return 'lifecycle';
|
||||||
|
if(kind==='control_boundary') return 'control';
|
||||||
|
if(kind==='terminal_status') return 'terminal';
|
||||||
|
return 'activity';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activityRowDisplayHint(kind, mode){
|
||||||
|
if(mode==='transparent_stream') return 'chronological_activity';
|
||||||
|
if(kind==='process_prose') return 'main_prose';
|
||||||
|
if(kind==='reasoning') return 'collapsed_thinking';
|
||||||
|
if(_isToolActivityKind(kind)) return 'tool_row';
|
||||||
|
if(kind==='lifecycle_status') return 'quiet_lifecycle_row';
|
||||||
|
if(kind==='control_boundary') return 'control_boundary_row';
|
||||||
|
if(kind==='terminal_status') return 'terminal_status_row';
|
||||||
|
return 'activity_row';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activityRowDisplayHints(kind){
|
||||||
|
return Object.freeze({
|
||||||
|
compact_worklog:_activityRowDisplayHint(kind,'compact_worklog'),
|
||||||
|
transparent_stream:_activityRowDisplayHint(kind,'transparent_stream'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _activitySceneRow(event, index, mode){
|
||||||
|
const payload=_own(event,'payload');
|
||||||
|
const kind=_cleanString(_own(event,'kind'))||'activity';
|
||||||
|
const status=_cleanString(_own(event,'status'))||null;
|
||||||
|
const text=_activityRowText(event);
|
||||||
|
const toolCallId=_activityRowToolId(event,kind);
|
||||||
|
const sanitizedPayload=_sanitizePayload(payload);
|
||||||
|
return Object.freeze({
|
||||||
|
row_id:_activityRowId(event,index),
|
||||||
|
order_index:index,
|
||||||
|
kind,
|
||||||
|
role:_activityRowRole(kind),
|
||||||
|
display_hint:_activityRowDisplayHint(kind,mode),
|
||||||
|
display_hints:_activityRowDisplayHints(kind),
|
||||||
|
source_event_type:_cleanString(_own(event,'source_event_type'))||null,
|
||||||
|
event_id:_cleanString(_own(event,'event_id'))||null,
|
||||||
|
local_id:_cleanString(_own(event,'local_id'))||null,
|
||||||
|
run_id:_cleanString(_own(event,'run_id'))||null,
|
||||||
|
stream_id:_cleanString(_own(event,'stream_id'))||null,
|
||||||
|
seq:_own(event,'seq')??null,
|
||||||
|
status,
|
||||||
|
created_at:_own(event,'created_at')??null,
|
||||||
|
identity:Object.freeze({
|
||||||
|
event_id:_cleanString(_own(event,'event_id'))||null,
|
||||||
|
local_id:_cleanString(_own(event,'local_id'))||null,
|
||||||
|
run_id:_cleanString(_own(event,'run_id'))||null,
|
||||||
|
stream_id:_cleanString(_own(event,'stream_id'))||null,
|
||||||
|
seq:_own(event,'seq')??null,
|
||||||
|
}),
|
||||||
|
group:_activityRowGroup(event,payload||{},index),
|
||||||
|
text,
|
||||||
|
thinking:_activityRowThinking(event,kind,text),
|
||||||
|
tool_call_id:toolCallId,
|
||||||
|
tool:_activityRowTool(event,kind,status,text,toolCallId),
|
||||||
|
payload:sanitizedPayload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectAssistantTurnAnchorActivityScene(input, options){
|
||||||
|
const anchor=_anchorFromProjectionInput(input);
|
||||||
|
const opts=(options&&typeof options==='object')?options:{};
|
||||||
|
const requestedMode=_cleanString(_own(opts,'mode'));
|
||||||
|
const mode=requestedMode==='transparent_stream'?'transparent_stream':'compact_worklog';
|
||||||
|
if(!anchor){
|
||||||
|
return Object.freeze({
|
||||||
|
version:'activity_scene_v1',
|
||||||
|
mode,
|
||||||
|
identity:Object.freeze({source_message_refs:Object.freeze([])}),
|
||||||
|
lifecycle:Object.freeze({}),
|
||||||
|
final_answer:'',
|
||||||
|
final_message_ref:null,
|
||||||
|
terminal_state:null,
|
||||||
|
activity_rows:Object.freeze([]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const rows=(Array.isArray(anchor.activity_events)?anchor.activity_events:[])
|
||||||
|
.map((event,index)=>_activitySceneRow(event,index,mode));
|
||||||
|
const lifecycle=_copyObject(anchor.lifecycle);
|
||||||
|
const content=anchor.content&&typeof anchor.content==='object'?anchor.content:{};
|
||||||
|
return Object.freeze({
|
||||||
|
version:'activity_scene_v1',
|
||||||
|
mode,
|
||||||
|
identity:_frozenIdentityCopy(anchor.identity||{}),
|
||||||
|
lifecycle:Object.freeze(lifecycle),
|
||||||
|
final_answer:typeof content.final_answer==='string'?content.final_answer:'',
|
||||||
|
final_message_ref:typeof content.final_message_ref==='string'?content.final_message_ref:null,
|
||||||
|
terminal_state:_cleanString(_own(lifecycle,'terminal_state'))||null,
|
||||||
|
activity_rows:Object.freeze(rows),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -845,7 +1101,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
ROOT.HermesAssistantTurnAnchors=Object.freeze({
|
ROOT.HermesAssistantTurnAnchors=Object.freeze({
|
||||||
version:'slice4-final-projection',
|
version:'slice5-activity-scene',
|
||||||
activityEventKinds:ACTIVITY_EVENT_KINDS,
|
activityEventKinds:ACTIVITY_EVENT_KINDS,
|
||||||
stateLayers:STATE_LAYERS,
|
stateLayers:STATE_LAYERS,
|
||||||
sourceEventClassification:SOURCE_EVENT_CLASSIFICATION,
|
sourceEventClassification:SOURCE_EVENT_CLASSIFICATION,
|
||||||
@@ -863,6 +1119,7 @@
|
|||||||
applyAssistantTurnAnchorSourceEvents,
|
applyAssistantTurnAnchorSourceEvents,
|
||||||
createAssistantTurnAnchorShadowSnapshot,
|
createAssistantTurnAnchorShadowSnapshot,
|
||||||
projectAssistantTurnAnchorSettledMessageFinalAnswer,
|
projectAssistantTurnAnchorSettledMessageFinalAnswer,
|
||||||
|
projectAssistantTurnAnchorActivityScene,
|
||||||
isAssistantTurnAnchorActivityKind,
|
isAssistantTurnAnchorActivityKind,
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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"] == "slice4-final-projection"
|
assert data["version"] == "slice5-activity-scene"
|
||||||
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"]
|
||||||
|
|||||||
@@ -111,6 +111,97 @@ console.log(JSON.stringify({{
|
|||||||
assert result.returncode == 0, result.stderr
|
assert result.returncode == 0, result.stderr
|
||||||
return json.loads(result.stdout)
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
def _activity_scene_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 registry = api.createAssistantTurnAnchorRegistry({{
|
||||||
|
session_id:'sid-scene',
|
||||||
|
turn_id:'turn-scene',
|
||||||
|
}});
|
||||||
|
api.applyAssistantTurnAnchorSourceEvents(registry, [
|
||||||
|
{{event:'token', payload:{{text:'progress'}}, event_id:'run-scene:1', seq:1}},
|
||||||
|
{{event:'reasoning', payload:{{text:'private thinking'}}, event_id:'run-scene:2', seq:2}},
|
||||||
|
{{event:'tool', payload:{{
|
||||||
|
tool_call_id:'tool-1',
|
||||||
|
name:'terminal',
|
||||||
|
args:{{command:'rg anchor static'}},
|
||||||
|
preview:'rg anchor static',
|
||||||
|
activityBurstId:7,
|
||||||
|
activitySegmentSeq:3,
|
||||||
|
assistant_msg_idx:12,
|
||||||
|
started_at:1781200000
|
||||||
|
}}, event_id:'run-scene:3', seq:3}},
|
||||||
|
{{event:'tool_update', payload:{{
|
||||||
|
tool_call_id:'tool-1',
|
||||||
|
name:'terminal',
|
||||||
|
text:'running',
|
||||||
|
preview:'searching workspace',
|
||||||
|
activityBurstId:7,
|
||||||
|
activitySegmentSeq:3,
|
||||||
|
assistant_msg_idx:12
|
||||||
|
}}, event_id:'run-scene:4', seq:4}},
|
||||||
|
{{event:'tool_complete', payload:{{
|
||||||
|
tool_call_id:'tool-1',
|
||||||
|
name:'terminal',
|
||||||
|
result:'done',
|
||||||
|
output:'done',
|
||||||
|
snippet:'done',
|
||||||
|
is_error:false,
|
||||||
|
duration:1.25,
|
||||||
|
activityBurstId:7,
|
||||||
|
activitySegmentSeq:3,
|
||||||
|
assistant_msg_idx:12
|
||||||
|
}}, event_id:'run-scene:5', seq:5}},
|
||||||
|
{{event:'done', payload:{{}}, event_id:'run-scene:6', seq:6}},
|
||||||
|
{{source_type:'settled_message', payload:{{role:'assistant', id:'message-scene', content:'final answer'}}}},
|
||||||
|
], {{run_id:'run-scene', stream_id:'stream-scene'}});
|
||||||
|
const compact = api.projectAssistantTurnAnchorActivityScene(registry, {{mode:'compact_worklog'}});
|
||||||
|
const transparent = api.projectAssistantTurnAnchorActivityScene(registry.anchor, {{mode:'transparent_stream'}});
|
||||||
|
const empty = api.projectAssistantTurnAnchorActivityScene(null, {{mode:'transparent_stream'}});
|
||||||
|
const seqlessRegistry = api.createAssistantTurnAnchorRegistry({{
|
||||||
|
session_id:'sid-seqless',
|
||||||
|
turn_id:'turn-seqless',
|
||||||
|
}});
|
||||||
|
api.applyAssistantTurnAnchorSourceEvents(seqlessRegistry, [
|
||||||
|
{{event:'tool', payload:{{tool_call_id:'tool-same', name:'terminal'}}}},
|
||||||
|
{{event:'tool_update', payload:{{tool_call_id:'tool-same', text:'running'}}}},
|
||||||
|
{{event:'tool_complete', payload:{{tool_call_id:'tool-same', result:'done'}}}},
|
||||||
|
]);
|
||||||
|
const seqless = api.projectAssistantTurnAnchorActivityScene(seqlessRegistry, {{mode:'compact_worklog'}});
|
||||||
|
const zeroRegistry = api.createAssistantTurnAnchorRegistry({{
|
||||||
|
session_id:'sid-zero',
|
||||||
|
turn_id:'turn-zero',
|
||||||
|
}});
|
||||||
|
api.applyAssistantTurnAnchorSourceEvents(zeroRegistry, [
|
||||||
|
{{event:'tool', payload:{{
|
||||||
|
tool_call_id:'tool-zero',
|
||||||
|
name:'terminal',
|
||||||
|
activityBurstId:0,
|
||||||
|
activitySegmentSeq:0,
|
||||||
|
assistant_msg_idx:0
|
||||||
|
}}, event_id:'run-zero:0', seq:0}},
|
||||||
|
]);
|
||||||
|
const zero = api.projectAssistantTurnAnchorActivityScene(zeroRegistry, {{mode:'compact_worklog'}});
|
||||||
|
console.log(JSON.stringify({{
|
||||||
|
version:api.version,
|
||||||
|
compact,
|
||||||
|
transparent,
|
||||||
|
empty,
|
||||||
|
seqless,
|
||||||
|
zero,
|
||||||
|
}}));
|
||||||
|
"""
|
||||||
|
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 _final_projection_snapshot() -> dict:
|
def _final_projection_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"
|
||||||
@@ -342,7 +433,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"] == "slice4-final-projection"
|
assert data["version"] == "slice5-activity-scene"
|
||||||
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"',
|
||||||
@@ -442,7 +533,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"] == "slice4-final-projection"
|
assert data["version"] == "slice5-activity-scene"
|
||||||
assert data["toolResults"] == [
|
assert data["toolResults"] == [
|
||||||
{"applied": True, "reason": None},
|
{"applied": True, "reason": None},
|
||||||
{"applied": True, "reason": None},
|
{"applied": True, "reason": None},
|
||||||
@@ -492,7 +583,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"] == "slice4-final-projection"
|
assert data["version"] == "slice5-activity-scene"
|
||||||
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"},
|
||||||
@@ -515,6 +606,131 @@ 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_activity_scene_projects_current_activity_events_for_both_render_modes():
|
||||||
|
data = _activity_scene_snapshot()
|
||||||
|
compact = data["compact"]
|
||||||
|
transparent = data["transparent"]
|
||||||
|
|
||||||
|
assert data["version"] == "slice5-activity-scene"
|
||||||
|
assert compact["version"] == "activity_scene_v1"
|
||||||
|
assert transparent["version"] == "activity_scene_v1"
|
||||||
|
assert compact["mode"] == "compact_worklog"
|
||||||
|
assert transparent["mode"] == "transparent_stream"
|
||||||
|
assert compact["final_answer"] == "final answer"
|
||||||
|
assert transparent["final_answer"] == "final answer"
|
||||||
|
assert compact["terminal_state"] == "completed"
|
||||||
|
assert transparent["terminal_state"] == "completed"
|
||||||
|
|
||||||
|
compact_rows = compact["activity_rows"]
|
||||||
|
transparent_rows = transparent["activity_rows"]
|
||||||
|
assert [row["row_id"] for row in compact_rows] == [
|
||||||
|
"run-scene:1",
|
||||||
|
"run-scene:2",
|
||||||
|
"run-scene:3",
|
||||||
|
"run-scene:4",
|
||||||
|
"run-scene:5",
|
||||||
|
"run-scene:6",
|
||||||
|
]
|
||||||
|
assert [row["row_id"] for row in transparent_rows] == [
|
||||||
|
row["row_id"] for row in compact_rows
|
||||||
|
]
|
||||||
|
assert [row["kind"] for row in compact_rows] == [
|
||||||
|
"process_prose",
|
||||||
|
"reasoning",
|
||||||
|
"tool_started",
|
||||||
|
"tool_updated",
|
||||||
|
"tool_completed",
|
||||||
|
"terminal_status",
|
||||||
|
]
|
||||||
|
assert [row["role"] for row in compact_rows] == [
|
||||||
|
"prose",
|
||||||
|
"thinking",
|
||||||
|
"tool",
|
||||||
|
"tool",
|
||||||
|
"tool",
|
||||||
|
"terminal",
|
||||||
|
]
|
||||||
|
assert [row["display_hint"] for row in compact_rows] == [
|
||||||
|
"main_prose",
|
||||||
|
"collapsed_thinking",
|
||||||
|
"tool_row",
|
||||||
|
"tool_row",
|
||||||
|
"tool_row",
|
||||||
|
"terminal_status_row",
|
||||||
|
]
|
||||||
|
assert all(row["display_hint"] == "chronological_activity" for row in transparent_rows)
|
||||||
|
assert compact_rows[0]["text"] == "progress"
|
||||||
|
assert compact_rows[0]["tool_call_id"] is None
|
||||||
|
assert compact_rows[2]["tool_call_id"] == "tool-1"
|
||||||
|
assert compact_rows[4]["text"] == "done"
|
||||||
|
assert compact_rows[1]["thinking"] == {
|
||||||
|
"text": "private thinking",
|
||||||
|
"preview": "private thinking",
|
||||||
|
"dedupe_key": "thinking:private thinking",
|
||||||
|
}
|
||||||
|
assert compact_rows[2]["group"] == {
|
||||||
|
"group_key": "segment:3",
|
||||||
|
"activity_burst_id": 7,
|
||||||
|
"activity_segment_seq": 3,
|
||||||
|
"assistant_msg_idx": 12,
|
||||||
|
}
|
||||||
|
assert compact_rows[2]["tool"] == {
|
||||||
|
"id": "tool-1",
|
||||||
|
"name": "terminal",
|
||||||
|
"args": {"command": "rg anchor static"},
|
||||||
|
"preview": "rg anchor static",
|
||||||
|
"snippet": "",
|
||||||
|
"result": None,
|
||||||
|
"output": None,
|
||||||
|
"done": False,
|
||||||
|
"is_error": False,
|
||||||
|
"duration": None,
|
||||||
|
"started_at": 1781200000,
|
||||||
|
"signature": 'terminal|tool-1|{"command":"rg anchor static"}',
|
||||||
|
}
|
||||||
|
assert compact_rows[4]["tool"]["done"] is True
|
||||||
|
assert compact_rows[4]["tool"]["is_error"] is False
|
||||||
|
assert compact_rows[4]["tool"]["duration"] == 1.25
|
||||||
|
assert compact_rows[4]["tool"]["snippet"] == "done"
|
||||||
|
assert compact_rows[4]["display_hints"] == {
|
||||||
|
"compact_worklog": "tool_row",
|
||||||
|
"transparent_stream": "chronological_activity",
|
||||||
|
}
|
||||||
|
seqless_ids = [row["row_id"] for row in data["seqless"]["activity_rows"]]
|
||||||
|
assert len(seqless_ids) == len(set(seqless_ids))
|
||||||
|
assert seqless_ids == [
|
||||||
|
"tool-same:tool:0",
|
||||||
|
"tool-same:tool_update:1",
|
||||||
|
"tool-same:tool_complete:2",
|
||||||
|
]
|
||||||
|
assert data["zero"]["activity_rows"][0]["group"] == {
|
||||||
|
"group_key": "segment:0",
|
||||||
|
"activity_burst_id": 0,
|
||||||
|
"activity_segment_seq": 0,
|
||||||
|
"assistant_msg_idx": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_activity_scene_is_renderer_neutral_and_empty_safe():
|
||||||
|
data = _activity_scene_snapshot()
|
||||||
|
compact = data["compact"]
|
||||||
|
empty = data["empty"]
|
||||||
|
|
||||||
|
assert "final answer" not in [row["text"] for row in compact["activity_rows"]]
|
||||||
|
assert compact["identity"]["session_id"] == "sid-scene"
|
||||||
|
assert compact["identity"]["run_id"] == "run-scene"
|
||||||
|
assert compact["identity"]["stream_id"] == "stream-scene"
|
||||||
|
assert empty == {
|
||||||
|
"version": "activity_scene_v1",
|
||||||
|
"mode": "transparent_stream",
|
||||||
|
"identity": {"source_message_refs": []},
|
||||||
|
"lifecycle": {},
|
||||||
|
"final_answer": "",
|
||||||
|
"final_message_ref": None,
|
||||||
|
"terminal_state": None,
|
||||||
|
"activity_rows": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_final_projection_routes_settled_assistant_message_through_anchor_owner():
|
def test_final_projection_routes_settled_assistant_message_through_anchor_owner():
|
||||||
data = _final_projection_snapshot()
|
data = _final_projection_snapshot()
|
||||||
@@ -522,7 +738,7 @@ def test_final_projection_routes_settled_assistant_message_through_anchor_owner(
|
|||||||
registry = projected["registry"]
|
registry = projected["registry"]
|
||||||
anchor = registry["anchor"]
|
anchor = registry["anchor"]
|
||||||
|
|
||||||
assert data["version"] == "slice4-final-projection"
|
assert data["version"] == "slice5-activity-scene"
|
||||||
assert projected["applied"] is True
|
assert projected["applied"] is True
|
||||||
assert projected["reason"] is None
|
assert projected["reason"] is None
|
||||||
assert projected["final_message_ref"] == "message-final"
|
assert projected["final_message_ref"] == "message-final"
|
||||||
@@ -589,8 +805,7 @@ def test_registry_instances_do_not_share_owner_state():
|
|||||||
assert isolated["stats"]["applied"] == 0
|
assert isolated["stats"]["applied"] == 0
|
||||||
assert isolated["anchor"]["activity_events"] == []
|
assert isolated["anchor"]["activity_events"] == []
|
||||||
|
|
||||||
|
def test_slice5_scene_projection_does_not_wire_activity_scene_into_rendering_hot_paths():
|
||||||
def test_slice4_projection_does_not_wire_registry_into_rendering_hot_paths():
|
|
||||||
helper_names = [
|
helper_names = [
|
||||||
"createAssistantTurnAnchorRegistry",
|
"createAssistantTurnAnchorRegistry",
|
||||||
"applyAssistantTurnAnchorNormalizedEvent",
|
"applyAssistantTurnAnchorNormalizedEvent",
|
||||||
@@ -603,3 +818,6 @@ def test_slice4_projection_does_not_wire_registry_into_rendering_hot_paths():
|
|||||||
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)
|
assert "projectAssistantTurnAnchorSettledMessageFinalAnswer" in _read(UI_JS)
|
||||||
|
assert "projectAssistantTurnAnchorActivityScene" not in _read(UI_JS)
|
||||||
|
assert "projectAssistantTurnAnchorActivityScene" not in _read(SESSIONS_JS)
|
||||||
|
assert "projectAssistantTurnAnchorActivityScene" not in _read(MESSAGES_JS)
|
||||||
|
|||||||
Reference in New Issue
Block a user