feat(anchor): add shadow source registry harness

This commit is contained in:
Frank Song committed 2026-06-12 15:52:28 +08:00
1 parent acd9d2e6f4
commit 65cb2a8b46
4 files changed
+154 -3

No files matched your search

@@ -6,6 +6,21 @@ It documents the current per-turn state layers and the event-shape contract that
future anchor phases must consume. It does not claim that anchors are wired into
streaming or rendering yet.
## RFC Phase Progress
- The #3962 Phase 0 scaffold shipped through #3977 / v0.51.359: inventory the
current state layers, encode the owner seed, and pin the source classification
contract.
- PR #3980 / v0.51.366 delivered the first RFC Phase 2 foundation: normalize
current live, replay, and settled source events into anchor-shaped events while
staying unwired from rendering.
- This slice advances RFC Phase 1 and Phase 2 together: it adds a local registry
owner plus a shadow source-feed harness that can combine live, replay,
settled, and in-flight observations into one anchor snapshot.
- The next independently reviewable boundary is Phase 3 settlement through the
anchor owner. `S.messages`, `INFLIGHT`, stream-local state, and DOM nodes remain
projection/cache layers until that wiring lands.
## State Layers
| Layer | Current surface | Phase 0 anchor policy |
@@ -67,6 +82,13 @@ This slice deliberately keeps the ownership boundary inert: `send()`,
can replace local renderer-owned state with this owner instead of adding another
parallel source of truth.
`HermesAssistantTurnAnchors.createAssistantTurnAnchorShadowSnapshot()` is the
shadow wiring harness for this slice. It accepts grouped `live_events`,
`replay_events` / `run_journal_events`, `settled_events`, and `inflight_events`,
feeds them through one local registry, and returns the resulting snapshot plus
per-source apply results. This gives later slices an invariant target without
making the current UI consume the owner yet.
## Source Event Classification
Phase 0 classifies current sources before changing render behavior:
+51 -1
View File
@@ -531,6 +531,55 @@
return list.map((event)=>applyAssistantTurnAnchorSourceEvent(registry,event,context));
}
function _eventsForShadowSource(sources, primaryKey, fallbackKey){
if(!sources||typeof sources!=='object') return [];
const primary=sources[primaryKey];
if(Array.isArray(primary)) return primary;
const fallback=fallbackKey?sources[fallbackKey]:null;
return Array.isArray(fallback)?fallback:[];
}
function _shadowSourceContext(context, sourceLayer){
return {
..._copyObject(context),
source_layer:sourceLayer,
};
}
function createAssistantTurnAnchorShadowSnapshot(input){
const opts=(input&&typeof input==='object')?input:{};
const anchorInput=(opts.anchor&&typeof opts.anchor==='object')?opts.anchor:opts;
const sources=(opts.sources&&typeof opts.sources==='object')?opts.sources:opts;
const context=(opts.context&&typeof opts.context==='object')?opts.context:{};
const registry=createAssistantTurnAnchorRegistry(anchorInput);
const results={
live:applyAssistantTurnAnchorSourceEvents(
registry,
_eventsForShadowSource(sources,'live_events'),
_shadowSourceContext(context,'live')
),
replay:applyAssistantTurnAnchorSourceEvents(
registry,
_eventsForShadowSource(sources,'replay_events','run_journal_events'),
_shadowSourceContext(context,'replay')
),
settled:applyAssistantTurnAnchorSourceEvents(
registry,
_eventsForShadowSource(sources,'settled_events'),
_shadowSourceContext(context,'settled')
),
inflight:applyAssistantTurnAnchorSourceEvents(
registry,
_eventsForShadowSource(sources,'inflight_events'),
_shadowSourceContext(context,'inflight')
),
};
return Object.freeze({
registry,
results:Object.freeze(results),
});
}
function createAssistantTurnAnchorSeed(input){
const opts=(input&&typeof input==='object')?input:{};
const sessionId=_cleanString(opts.session_id);
@@ -593,7 +642,7 @@
}
ROOT.HermesAssistantTurnAnchors=Object.freeze({
version:'slice3-registry',
version:'slice3-registry-shadow',
activityEventKinds:ACTIVITY_EVENT_KINDS,
stateLayers:STATE_LAYERS,
sourceEventClassification:SOURCE_EVENT_CLASSIFICATION,
@@ -607,6 +656,7 @@
applyAssistantTurnAnchorNormalizedEvent,
applyAssistantTurnAnchorSourceEvent,
applyAssistantTurnAnchorSourceEvents,
createAssistantTurnAnchorShadowSnapshot,
isAssistantTurnAnchorActivityKind,
});
})();
@@ -180,7 +180,7 @@ def test_normalizer_maps_live_and_replay_to_same_anchor_event_identity():
live = data["liveToken"]
replay = data["replayToken"]
assert data["version"] == "slice3-registry"
assert data["version"] == "slice3-registry-shadow"
assert live["classification"] == "activity"
assert live["dedupe_key"] == 'event_id:"run-1:7"'
assert replay["dedupe_key"] == live["dedupe_key"]
@@ -63,12 +63,61 @@ console.log(JSON.stringify({{
return json.loads(result.stdout)
def _shadow_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 shadow = api.createAssistantTurnAnchorShadowSnapshot({{
anchor:{{
session_id:'sid-shadow',
turn_id:'turn-shadow',
}},
context:{{
run_id:'run-shadow',
stream_id:'stream-shadow',
}},
sources:{{
live_events:[
{{type:'token', data:'{{"text":"live token"}}', lastEventId:'run-shadow:1', created_at:'2026-06-11T00:00:01Z'}},
],
replay_events:[
{{event:'token', payload:{{text:'replay duplicate'}}, event_id:'run-shadow:1', seq:1}},
{{event:'tool_complete', payload:{{tool_call_id:'tool-1', result:'ok'}}, event_id:'run-shadow:2', seq:2}},
],
settled_events:[
{{source_type:'settled_message', payload:{{role:'assistant', id:'message-shadow', content:'shadow final'}}}},
],
inflight_events:[
{{source_type:'inflight_snapshot', payload:{{status:'restoring'}}}},
],
}},
}});
console.log(JSON.stringify({{
version:api.version,
registry:shadow.registry,
results:Object.fromEntries(Object.entries(shadow.results).map(([key, value]) => [
key,
value.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 data["version"] == "slice3-registry-shadow"
assert [item["reason"] for item in data["results"][:2]] == [None, "duplicate"]
assert registry["event_index"]["dedupe_keys"][:2] == [
'event_id:"run-1:1"',
@@ -121,6 +170,35 @@ def test_registry_updates_lifecycle_and_settled_final_projection():
assert anchor["usage"] == {"input_tokens": 8, "output_tokens": 13}
def test_shadow_snapshot_feeds_current_source_families_into_one_registry_owner():
data = _shadow_snapshot()
registry = data["registry"]
anchor = registry["anchor"]
assert data["version"] == "slice3-registry-shadow"
assert data["results"]["live"] == [{"applied": True, "reason": None}]
assert data["results"]["replay"] == [
{"applied": False, "reason": "duplicate"},
{"applied": True, "reason": None},
]
assert data["results"]["settled"] == [{"applied": True, "reason": None}]
assert data["results"]["inflight"] == [{"applied": True, "reason": None}]
assert registry["stats"]["applied"] == 4
assert registry["stats"]["skipped_duplicate"] == 1
assert anchor["identity"]["run_id"] == "run-shadow"
assert anchor["identity"]["stream_id"] == "stream-shadow"
assert [event["kind"] for event in anchor["activity_events"]] == [
"process_prose",
"tool_completed",
]
assert [event["source_event_type"] for event in anchor["metadata_events"]] == [
"settled_message",
"inflight_snapshot",
]
assert anchor["content"]["final_answer"] == "shadow final"
def test_registry_instances_do_not_share_owner_state():
data = _registry_snapshot()
isolated = data["isolated"]
@@ -137,6 +215,7 @@ def test_slice3_registry_is_still_unwired_from_rendering_hot_paths():
"applyAssistantTurnAnchorNormalizedEvent",
"applyAssistantTurnAnchorSourceEvent",
"applyAssistantTurnAnchorSourceEvents",
"createAssistantTurnAnchorShadowSnapshot",
]
for helper in helper_names:
assert helper not in _read(UI_JS)