feat(anchor): add inert turn registry skeleton
This commit is contained in:
1 parent
9e96f5f6ad
commit
56a89ef11e
4 files changed
+363
-6
No files matched your search
@@ -37,6 +37,36 @@ same event-envelope key. This is still inert: `send()`, `attachLiveStream()`,
|
|||||||
`renderMessages()`, settlement restore, `S.messages`, `INFLIGHT`, and the DOM do
|
`renderMessages()`, settlement restore, `S.messages`, `INFLIGHT`, and the DOM do
|
||||||
not consume the helper yet.
|
not consume the helper yet.
|
||||||
|
|
||||||
|
## Slice 3 Registry / Owner Skeleton
|
||||||
|
|
||||||
|
`HermesAssistantTurnAnchors.createAssistantTurnAnchorRegistry()` creates a local
|
||||||
|
owner object for one assistant turn. The registry contains the anchor seed, a
|
||||||
|
dedupe index, and application stats. It is not a global store and is not wired
|
||||||
|
into current runtime, session, or renderer code.
|
||||||
|
|
||||||
|
`HermesAssistantTurnAnchors.applyAssistantTurnAnchorSourceEvent()` and
|
||||||
|
`applyAssistantTurnAnchorSourceEvents()` normalize incoming source events, apply
|
||||||
|
the same event-envelope dedupe rule, and route events into one owner:
|
||||||
|
|
||||||
|
- `activity_events` for visible assistant activity such as prose, reasoning,
|
||||||
|
tools, control boundaries, and terminal status
|
||||||
|
- `artifacts` for workspace/file references
|
||||||
|
- `side_effects` for persisted state side effects
|
||||||
|
- `metadata_events` for settlement/session metadata such as `settled_message`
|
||||||
|
- `transport_events` for transport-only signals such as `stream_end`
|
||||||
|
|
||||||
|
The registry may fill missing `run_id` / `stream_id` identity from the first
|
||||||
|
matching normalized event, update lifecycle on terminal status, and copy the
|
||||||
|
settled assistant message into `content.final_answer`. It rejects mismatched
|
||||||
|
session or turn identity and skips duplicate live + replay observations by the
|
||||||
|
same dedupe key.
|
||||||
|
|
||||||
|
This slice deliberately keeps the ownership boundary inert: `send()`,
|
||||||
|
`attachLiveStream()`, replay hydration, `renderMessages()`, `S.messages`,
|
||||||
|
`INFLIGHT`, and DOM continuity still do not consume the registry. Later slices
|
||||||
|
can replace local renderer-owned state with this owner instead of adding another
|
||||||
|
parallel source of truth.
|
||||||
|
|
||||||
## Source Event Classification
|
## Source Event Classification
|
||||||
|
|
||||||
Phase 0 classifies current sources before changing render behavior:
|
Phase 0 classifies current sources before changing render behavior:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Stable Assistant Turn Anchors scaffold (#3926).
|
// Stable Assistant Turn Anchors scaffold (#3926).
|
||||||
//
|
//
|
||||||
// This file is intentionally inert: it defines the current ownership inventory,
|
// This file is intentionally inert: it defines the current ownership inventory,
|
||||||
// event classifications, and small pure helpers, but it does not register
|
// event classifications, and small owner helpers, but it does not register
|
||||||
// anchors or change any renderer. Later phases can wire these helpers into
|
// anchors globally or change any renderer. Later phases can wire these helpers into
|
||||||
// send(), attachLiveStream(), replay hydration, and renderMessages().
|
// send(), attachLiveStream(), replay hydration, and renderMessages().
|
||||||
(function(){
|
(function(){
|
||||||
const ROOT=(typeof window!=='undefined')?window:globalThis;
|
const ROOT=(typeof window!=='undefined')?window:globalThis;
|
||||||
@@ -374,6 +374,163 @@
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _copyObject(value){
|
||||||
|
if(!value||typeof value!=='object'||Array.isArray(value)) return {};
|
||||||
|
return {...value};
|
||||||
|
}
|
||||||
|
|
||||||
|
function _registryAnchor(registry){
|
||||||
|
return registry&&typeof registry==='object'&®istry.anchor&&typeof registry.anchor==='object'
|
||||||
|
?registry.anchor
|
||||||
|
:null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _registryContext(registry, context){
|
||||||
|
const anchor=_registryAnchor(registry);
|
||||||
|
const identity=anchor&&anchor.identity?anchor.identity:{};
|
||||||
|
return {
|
||||||
|
..._copyObject(context),
|
||||||
|
session_id:identity.session_id||_cleanString(context&&context.session_id),
|
||||||
|
turn_id:identity.turn_id||_cleanString(context&&context.turn_id),
|
||||||
|
run_id:identity.run_id||_cleanString(context&&context.run_id),
|
||||||
|
stream_id:identity.stream_id||_cleanString(context&&context.stream_id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function _eventBelongsToAnchor(anchor, event){
|
||||||
|
const identity=anchor.identity||{};
|
||||||
|
const sessionId=_cleanString(event.session_id);
|
||||||
|
if(sessionId&&identity.session_id&&sessionId!==identity.session_id) return false;
|
||||||
|
const turnId=_cleanString(event.turn_id);
|
||||||
|
if(turnId&&identity.turn_id&&turnId!==identity.turn_id) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ensureRegistryShape(registry){
|
||||||
|
const anchor=_registryAnchor(registry);
|
||||||
|
if(!anchor) throw new Error('assistant turn anchor registry requires anchor');
|
||||||
|
registry.event_index=registry.event_index&&typeof registry.event_index==='object'
|
||||||
|
?registry.event_index
|
||||||
|
:{};
|
||||||
|
if(!Array.isArray(registry.event_index.dedupe_keys)) registry.event_index.dedupe_keys=[];
|
||||||
|
registry.stats=registry.stats&&typeof registry.stats==='object'?registry.stats:{};
|
||||||
|
registry.stats.applied=Number(registry.stats.applied)||0;
|
||||||
|
registry.stats.skipped_duplicate=Number(registry.stats.skipped_duplicate)||0;
|
||||||
|
registry.stats.skipped_excluded=Number(registry.stats.skipped_excluded)||0;
|
||||||
|
registry.stats.skipped_mismatched=Number(registry.stats.skipped_mismatched)||0;
|
||||||
|
if(!Array.isArray(anchor.metadata_events)) anchor.metadata_events=[];
|
||||||
|
if(!Array.isArray(anchor.transport_events)) anchor.transport_events=[];
|
||||||
|
return anchor;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _syncAnchorIdentity(anchor, event){
|
||||||
|
const identity=anchor.identity||{};
|
||||||
|
if(!identity.run_id&&_cleanString(event.run_id)) identity.run_id=_cleanString(event.run_id);
|
||||||
|
if(!identity.stream_id&&_cleanString(event.stream_id)) identity.stream_id=_cleanString(event.stream_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _firstTextValue(){
|
||||||
|
for(let i=0;i<arguments.length;i+=1){
|
||||||
|
const value=arguments[i];
|
||||||
|
if(typeof value==='string'&&value.length>0) return value;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _messageRefFromPayload(payload, event){
|
||||||
|
return _firstTextValue(
|
||||||
|
payload&&payload.message_id,
|
||||||
|
payload&&payload.id,
|
||||||
|
payload&&payload.local_id,
|
||||||
|
event&&event.local_id,
|
||||||
|
event&&event.event_id
|
||||||
|
)||null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateLifecycleFromEvent(anchor, event){
|
||||||
|
const lifecycle=anchor.lifecycle||{};
|
||||||
|
if(!lifecycle.started_at&&event.created_at) lifecycle.started_at=event.created_at;
|
||||||
|
if((!lifecycle.status||lifecycle.status==='created')&&event.status==='running'){
|
||||||
|
lifecycle.status='running';
|
||||||
|
}
|
||||||
|
if(event.kind==='terminal_status'){
|
||||||
|
const terminal=_cleanString(event.status)||'completed';
|
||||||
|
lifecycle.status=terminal;
|
||||||
|
lifecycle.terminal_state=terminal;
|
||||||
|
lifecycle.completed_at=event.created_at||lifecycle.completed_at||null;
|
||||||
|
}
|
||||||
|
anchor.lifecycle=lifecycle;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateContentFromMetadata(anchor, event){
|
||||||
|
const payload=event.payload||{};
|
||||||
|
if(event.source_event_type==='usage'){
|
||||||
|
anchor.usage=_copyObject(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(event.source_event_type!=='settled_message') return;
|
||||||
|
if(payload.role&&payload.role!=='assistant') return;
|
||||||
|
const finalAnswer=_firstTextValue(payload.content,payload.text,payload.final_answer,payload.answer);
|
||||||
|
if(finalAnswer){
|
||||||
|
anchor.content=anchor.content||{};
|
||||||
|
anchor.content.final_answer=finalAnswer;
|
||||||
|
anchor.content.final_message_ref=_messageRefFromPayload(payload,event);
|
||||||
|
}
|
||||||
|
if(payload._turnUsage&&typeof payload._turnUsage==='object') anchor.usage=_copyObject(payload._turnUsage);
|
||||||
|
if(payload.usage&&typeof payload.usage==='object') anchor.usage=_copyObject(payload.usage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _routeAnchorEvent(anchor, normalized){
|
||||||
|
const event=normalized.anchor_event;
|
||||||
|
if(normalized.classification==='activity'){
|
||||||
|
anchor.activity_events.push(event);
|
||||||
|
_updateLifecycleFromEvent(anchor,event);
|
||||||
|
}else if(normalized.classification==='artifact'){
|
||||||
|
anchor.artifacts.push(event);
|
||||||
|
}else if(normalized.classification==='side_effect'){
|
||||||
|
anchor.side_effects.push(event);
|
||||||
|
}else if(normalized.classification==='metadata'){
|
||||||
|
anchor.metadata_events.push(event);
|
||||||
|
_updateContentFromMetadata(anchor,event);
|
||||||
|
}else if(normalized.classification==='transport'){
|
||||||
|
anchor.transport_events.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAssistantTurnAnchorNormalizedEvent(registry, normalized){
|
||||||
|
const anchor=_ensureRegistryShape(registry);
|
||||||
|
const item=(normalized&&typeof normalized==='object')?normalized:{};
|
||||||
|
const event=item.anchor_event;
|
||||||
|
if(!event){
|
||||||
|
registry.stats.skipped_excluded+=1;
|
||||||
|
return Object.freeze({applied:false,reason:'excluded',normalized:item});
|
||||||
|
}
|
||||||
|
if(!_eventBelongsToAnchor(anchor,event)){
|
||||||
|
registry.stats.skipped_mismatched+=1;
|
||||||
|
return Object.freeze({applied:false,reason:'mismatched_anchor',normalized:item});
|
||||||
|
}
|
||||||
|
const dedupeKey=_cleanString(item.dedupe_key)||assistantTurnAnchorEventDedupeKey(event);
|
||||||
|
if(dedupeKey&®istry.event_index.dedupe_keys.indexOf(dedupeKey)!==-1){
|
||||||
|
registry.stats.skipped_duplicate+=1;
|
||||||
|
return Object.freeze({applied:false,reason:'duplicate',normalized:item});
|
||||||
|
}
|
||||||
|
if(dedupeKey) registry.event_index.dedupe_keys.push(dedupeKey);
|
||||||
|
_syncAnchorIdentity(anchor,event);
|
||||||
|
_routeAnchorEvent(anchor,item);
|
||||||
|
registry.stats.applied+=1;
|
||||||
|
return Object.freeze({applied:true,reason:null,normalized:item});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAssistantTurnAnchorSourceEvent(registry, input, context){
|
||||||
|
const normalized=normalizeAssistantTurnAnchorSourceEvent(input,_registryContext(registry,context));
|
||||||
|
return applyAssistantTurnAnchorNormalizedEvent(registry,normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAssistantTurnAnchorSourceEvents(registry, events, context){
|
||||||
|
const list=Array.isArray(events)?events:[];
|
||||||
|
return list.map((event)=>applyAssistantTurnAnchorSourceEvent(registry,event,context));
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -407,6 +564,8 @@
|
|||||||
activity_events:[],
|
activity_events:[],
|
||||||
artifacts:[],
|
artifacts:[],
|
||||||
side_effects:[],
|
side_effects:[],
|
||||||
|
metadata_events:[],
|
||||||
|
transport_events:[],
|
||||||
usage:null,
|
usage:null,
|
||||||
presentation_state:{
|
presentation_state:{
|
||||||
compact_worklog:{expanded:false},
|
compact_worklog:{expanded:false},
|
||||||
@@ -416,8 +575,25 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createAssistantTurnAnchorRegistry(input){
|
||||||
|
const anchor=createAssistantTurnAnchorSeed(input);
|
||||||
|
return {
|
||||||
|
identity:anchor.identity,
|
||||||
|
anchor,
|
||||||
|
event_index:{
|
||||||
|
dedupe_keys:[],
|
||||||
|
},
|
||||||
|
stats:{
|
||||||
|
applied:0,
|
||||||
|
skipped_duplicate:0,
|
||||||
|
skipped_excluded:0,
|
||||||
|
skipped_mismatched:0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
ROOT.HermesAssistantTurnAnchors=Object.freeze({
|
ROOT.HermesAssistantTurnAnchors=Object.freeze({
|
||||||
version:'slice2-normalizer',
|
version:'slice3-registry',
|
||||||
activityEventKinds:ACTIVITY_EVENT_KINDS,
|
activityEventKinds:ACTIVITY_EVENT_KINDS,
|
||||||
stateLayers:STATE_LAYERS,
|
stateLayers:STATE_LAYERS,
|
||||||
sourceEventClassification:SOURCE_EVENT_CLASSIFICATION,
|
sourceEventClassification:SOURCE_EVENT_CLASSIFICATION,
|
||||||
@@ -427,6 +603,10 @@
|
|||||||
classifyAssistantTurnAnchorSourceEvent,
|
classifyAssistantTurnAnchorSourceEvent,
|
||||||
normalizeAssistantTurnAnchorSourceEvent,
|
normalizeAssistantTurnAnchorSourceEvent,
|
||||||
normalizeAssistantTurnAnchorSourceEvents,
|
normalizeAssistantTurnAnchorSourceEvents,
|
||||||
|
createAssistantTurnAnchorRegistry,
|
||||||
|
applyAssistantTurnAnchorNormalizedEvent,
|
||||||
|
applyAssistantTurnAnchorSourceEvent,
|
||||||
|
applyAssistantTurnAnchorSourceEvents,
|
||||||
isAssistantTurnAnchorActivityKind,
|
isAssistantTurnAnchorActivityKind,
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Slice 2 normalizer tests for Stable Assistant Turn Anchors (#3926)."""
|
"""Normalizer tests for Stable Assistant Turn Anchors (#3926)."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -180,7 +180,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"] == "slice2-normalizer"
|
assert data["version"] == "slice3-registry"
|
||||||
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"]
|
||||||
@@ -321,10 +321,13 @@ def test_structured_fallback_dedupe_keys_do_not_collide_on_delimiters():
|
|||||||
assert data["localCollisionA"] != data["localCollisionB"]
|
assert data["localCollisionA"] != data["localCollisionB"]
|
||||||
|
|
||||||
|
|
||||||
def test_slice2_normalizer_is_still_unwired_from_rendering_hot_paths():
|
def test_normalizer_and_registry_helpers_are_still_unwired_from_rendering_hot_paths():
|
||||||
helper_names = [
|
helper_names = [
|
||||||
"normalizeAssistantTurnAnchorSourceEvent",
|
"normalizeAssistantTurnAnchorSourceEvent",
|
||||||
"normalizeAssistantTurnAnchorSourceEvents",
|
"normalizeAssistantTurnAnchorSourceEvents",
|
||||||
|
"createAssistantTurnAnchorRegistry",
|
||||||
|
"applyAssistantTurnAnchorSourceEvent",
|
||||||
|
"applyAssistantTurnAnchorSourceEvents",
|
||||||
]
|
]
|
||||||
for helper in helper_names:
|
for helper in helper_names:
|
||||||
assert helper not in _read(UI_JS)
|
assert helper not in _read(UI_JS)
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
"""Slice 3 registry tests for Stable Assistant Turn Anchors (#3926)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
ANCHORS_JS = REPO / "static" / "assistant_turn_anchors.js"
|
||||||
|
MESSAGES_JS = REPO / "static" / "messages.js"
|
||||||
|
UI_JS = REPO / "static" / "ui.js"
|
||||||
|
SESSIONS_JS = REPO / "static" / "sessions.js"
|
||||||
|
NODE = shutil.which("node")
|
||||||
|
|
||||||
|
|
||||||
|
def _read(path: Path) -> str:
|
||||||
|
return path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _registry_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-1',
|
||||||
|
turn_id:'turn-1',
|
||||||
|
}});
|
||||||
|
const results = api.applyAssistantTurnAnchorSourceEvents(registry, [
|
||||||
|
{{type:'token', data:'{{"text":"live token"}}', lastEventId:'run-1:1', created_at:'2026-06-11T00:00:01Z'}},
|
||||||
|
{{event:'token', payload:{{text:'replay token'}}, event_id:'run-1:1', seq:1}},
|
||||||
|
{{event:'reasoning', payload:{{text:'thinking'}}, event_id:'run-1:2', seq:2}},
|
||||||
|
{{event:'artifact_reference', payload:{{path:'answer.txt', kind:'workspace_file'}}, event_id:'run-1:3', seq:3}},
|
||||||
|
{{event:'state_saved', payload:{{kind:'memory', name:'session-state'}}, event_id:'run-1:4', seq:4}},
|
||||||
|
{{event:'stream_end', payload:{{}}, event_id:'run-1:5', seq:5}},
|
||||||
|
{{event:'done', payload:{{}}, event_id:'run-1:6', seq:6, created_at:'2026-06-11T00:00:06Z'}},
|
||||||
|
{{source_type:'settled_message', payload:{{role:'assistant', id:'message-final', content:'final answer', _turnUsage:{{input_tokens:8, output_tokens:13}}}}}},
|
||||||
|
], {{run_id:'run-1', stream_id:'stream-1'}});
|
||||||
|
|
||||||
|
const isolated = api.createAssistantTurnAnchorRegistry({{session_id:'sid-1', turn_id:'turn-2'}});
|
||||||
|
api.applyAssistantTurnAnchorSourceEvent(registry, {{
|
||||||
|
event:'token',
|
||||||
|
payload:{{text:'wrong session', session_id:'sid-2'}},
|
||||||
|
event_id:'run-1:7',
|
||||||
|
seq:7,
|
||||||
|
}}, {{run_id:'run-1'}});
|
||||||
|
|
||||||
|
console.log(JSON.stringify({{
|
||||||
|
version:api.version,
|
||||||
|
registry,
|
||||||
|
isolated,
|
||||||
|
results:results.map((item)=>({{applied:item.applied, reason:item.reason}})),
|
||||||
|
}}));
|
||||||
|
"""
|
||||||
|
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 test_registry_owns_one_anchor_and_dedupes_live_plus_replay_events():
|
||||||
|
data = _registry_snapshot()
|
||||||
|
registry = data["registry"]
|
||||||
|
anchor = registry["anchor"]
|
||||||
|
|
||||||
|
assert data["version"] == "slice3-registry"
|
||||||
|
assert [item["reason"] for item in data["results"][:2]] == [None, "duplicate"]
|
||||||
|
assert registry["event_index"]["dedupe_keys"][:2] == [
|
||||||
|
"event_id:run-1:1",
|
||||||
|
"event_id:run-1:2",
|
||||||
|
]
|
||||||
|
assert registry["stats"]["applied"] == 7
|
||||||
|
assert registry["stats"]["skipped_duplicate"] == 1
|
||||||
|
assert registry["stats"]["skipped_mismatched"] == 1
|
||||||
|
|
||||||
|
assert anchor["identity"]["session_id"] == "sid-1"
|
||||||
|
assert anchor["identity"]["turn_id"] == "turn-1"
|
||||||
|
assert anchor["identity"]["run_id"] == "run-1"
|
||||||
|
assert anchor["identity"]["stream_id"] == "stream-1"
|
||||||
|
assert [event["kind"] for event in anchor["activity_events"]] == [
|
||||||
|
"process_prose",
|
||||||
|
"reasoning",
|
||||||
|
"terminal_status",
|
||||||
|
]
|
||||||
|
assert anchor["activity_events"][0]["payload"] == {"text": "live token"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_routes_activity_artifacts_side_effects_metadata_and_transport():
|
||||||
|
data = _registry_snapshot()
|
||||||
|
anchor = data["registry"]["anchor"]
|
||||||
|
|
||||||
|
assert len(anchor["artifacts"]) == 1
|
||||||
|
assert anchor["artifacts"][0]["source_event_type"] == "artifact_reference"
|
||||||
|
assert anchor["artifacts"][0]["payload"] == {
|
||||||
|
"kind": "workspace_file",
|
||||||
|
"path": "answer.txt",
|
||||||
|
}
|
||||||
|
assert len(anchor["side_effects"]) == 1
|
||||||
|
assert anchor["side_effects"][0]["source_event_type"] == "state_saved"
|
||||||
|
assert len(anchor["metadata_events"]) == 1
|
||||||
|
assert anchor["metadata_events"][0]["source_event_type"] == "settled_message"
|
||||||
|
assert len(anchor["transport_events"]) == 1
|
||||||
|
assert anchor["transport_events"][0]["source_event_type"] == "stream_end"
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_updates_lifecycle_and_settled_final_projection():
|
||||||
|
data = _registry_snapshot()
|
||||||
|
anchor = data["registry"]["anchor"]
|
||||||
|
|
||||||
|
assert anchor["lifecycle"]["status"] == "completed"
|
||||||
|
assert anchor["lifecycle"]["terminal_state"] == "completed"
|
||||||
|
assert anchor["lifecycle"]["started_at"] == "2026-06-11T00:00:01Z"
|
||||||
|
assert anchor["lifecycle"]["completed_at"] == "2026-06-11T00:00:06Z"
|
||||||
|
assert anchor["content"]["final_answer"] == "final answer"
|
||||||
|
assert anchor["content"]["final_message_ref"] == "message-final"
|
||||||
|
assert anchor["usage"] == {"input_tokens": 8, "output_tokens": 13}
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_instances_do_not_share_owner_state():
|
||||||
|
data = _registry_snapshot()
|
||||||
|
isolated = data["isolated"]
|
||||||
|
|
||||||
|
assert isolated["identity"]["turn_id"] == "turn-2"
|
||||||
|
assert isolated["event_index"]["dedupe_keys"] == []
|
||||||
|
assert isolated["stats"]["applied"] == 0
|
||||||
|
assert isolated["anchor"]["activity_events"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_slice3_registry_is_still_unwired_from_rendering_hot_paths():
|
||||||
|
helper_names = [
|
||||||
|
"createAssistantTurnAnchorRegistry",
|
||||||
|
"applyAssistantTurnAnchorNormalizedEvent",
|
||||||
|
"applyAssistantTurnAnchorSourceEvent",
|
||||||
|
"applyAssistantTurnAnchorSourceEvents",
|
||||||
|
]
|
||||||
|
for helper in helper_names:
|
||||||
|
assert helper not in _read(UI_JS)
|
||||||
|
assert helper not in _read(SESSIONS_JS)
|
||||||
|
assert helper not in _read(MESSAGES_JS)
|
||||||
Reference in new issue
Block a user