Release LW stage — v0.51.359 (#3962 assistant turn anchor phase 0 scaffold) (#3977)
Some checks failed
Release & Docker / release (push) Has been cancelled
Some checks failed
Release & Docker / release (push) Has been cancelled
* feat(anchor): add stable assistant turn phase 0 scaffold * chore: stamp v0.51.359 — Release LW (assistant turn anchor phase 0 scaffold, #3962) --------- Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <agent@local>
This commit is contained in:
@@ -3,8 +3,11 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.359] — 2026-06-11 — Release LW (assistant turn anchor phase 0 scaffold)
|
||||
|
||||
### Added
|
||||
|
||||
- **Internal Stable Assistant Turn Anchors Phase 0 scaffold.** The browser now ships an inert `HermesAssistantTurnAnchors` helper surface plus a documented state-layer inventory for #3926, pinning current live/replay/settled source classifications and event dedupe precedence without changing visible chat rendering.
|
||||
- **New RFC: Stable Assistant Turn Anchors for Live-to-Final rendering.** Defines a frontend presentation/reconciliation model for anchoring one assistant turn across live streaming, settlement, replay/reload/recovery, Compact Worklog, Transparent Stream, terminal states, artifacts, and side effects. (#3926)
|
||||
|
||||
## [v0.51.358] — 2026-06-11 — Release LV (first-run password bootstrap hardening)
|
||||
|
||||
@@ -31,6 +31,10 @@ does not change runtime behavior, maintainer policy, bot behavior, or CI gates.
|
||||
proposed product model for long-running assistant replies, live process text,
|
||||
tool activity, recovery, terminal outcomes, and final-answer boundaries. Start
|
||||
here for UI/UX changes to running-session assistant reply rendering.
|
||||
- [`docs/architecture/stable-assistant-turn-anchor-phase0.md`](architecture/stable-assistant-turn-anchor-phase0.md):
|
||||
current Phase 0 inventory for the Stable Assistant Turn Anchors work under
|
||||
#3926. Use this before wiring anchor helpers into live SSE, replay,
|
||||
settlement, `INFLIGHT`, or `renderMessages()` paths.
|
||||
- [`docs/rfcs/canonical-session-resolution.md`](rfcs/canonical-session-resolution.md):
|
||||
proposed contract for resolving URL routes, query parameters, localStorage,
|
||||
sidebar rows, and compression-lineage IDs to one canonical visible session
|
||||
|
||||
52
docs/architecture/stable-assistant-turn-anchor-phase0.md
Normal file
52
docs/architecture/stable-assistant-turn-anchor-phase0.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Stable Assistant Turn Anchors Phase 0 Inventory
|
||||
|
||||
This inventory implements the first non-visual slice of
|
||||
[`stable-assistant-turn-anchors.md`](../rfcs/stable-assistant-turn-anchors.md).
|
||||
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.
|
||||
|
||||
## State Layers
|
||||
|
||||
| Layer | Current surface | Phase 0 anchor policy |
|
||||
| --- | --- | --- |
|
||||
| RuntimeAdapter / run-journal Event Envelope | `event_id`, `run_id`, `seq`, `Last-Event-ID` / `after_seq` | Preferred identity and replay dedupe source. |
|
||||
| Run journal replay events | `read_run_events()`, `_replay_run_journal`, `runtime_journal_snapshot` | Durable replay hydration source before browser caches. |
|
||||
| Server settled transcript | `/api/session` messages and metadata | Settlement updates final answer and terminal state on an existing turn. |
|
||||
| `S.messages` | Browser transcript projection consumed by `renderMessages()` | Projection/cache, not a second semantic owner. |
|
||||
| `INFLIGHT` | Browser recovery cache and persisted localStorage state | Recovery fallback only; does not outrank journal or settled transcript. |
|
||||
| Stream closure state | `attachLiveStream()` local assistant text, reasoning text, parser target, tool state | Hot-path write buffer; future phases normalize this into anchor events. |
|
||||
| Live DOM | `#liveAssistantTurn`, Worklog rows, tool cards, Thinking cards | Renderer output only; DOM survival is not semantic truth. |
|
||||
|
||||
The same inventory is encoded in `static/assistant_turn_anchors.js` as
|
||||
`HermesAssistantTurnAnchors.stateLayers` so tests can pin the current authority
|
||||
order.
|
||||
|
||||
## Source Event Classification
|
||||
|
||||
Phase 0 classifies current sources before changing render behavior:
|
||||
|
||||
- activity: `token`, `interim_assistant`, `reasoning`, `tool`,
|
||||
`tool_complete`, `tool_update`, `compressing`, `compressed`, `approval`,
|
||||
`clarify`, `pending_steer_leftover`, `goal_continue`, `done`, `cancel`,
|
||||
`error`, `apperror`
|
||||
- artifact: `artifact_reference`
|
||||
- side effect: `state_saved`
|
||||
- metadata: `usage`, `title`, `settled_message`, `runtime_journal_snapshot`,
|
||||
`inflight_snapshot`
|
||||
- transport: `stream_end`
|
||||
|
||||
Future phases may add sources, but every source must choose one of these classes
|
||||
or explicitly mark itself `excluded`.
|
||||
|
||||
## Dedupe Invariant
|
||||
|
||||
Anchor event dedupe is intentionally independent of visible text and timestamps.
|
||||
The Phase 0 helper uses this order:
|
||||
|
||||
1. `event_id`
|
||||
2. `run_id + seq`
|
||||
3. `session_id + local_id` as a browser fallback
|
||||
|
||||
This mirrors the RuntimeAdapter Event Envelope and keeps the browser aligned
|
||||
with run-journal replay while the anchor registry is still unwired.
|
||||
199
static/assistant_turn_anchors.js
Normal file
199
static/assistant_turn_anchors.js
Normal file
@@ -0,0 +1,199 @@
|
||||
// Stable Assistant Turn Anchors Phase 0 scaffold (#3926).
|
||||
//
|
||||
// This file is intentionally inert: it defines the current ownership inventory,
|
||||
// event classifications, and small pure helpers, but it does not register
|
||||
// anchors or change any renderer. Later phases can wire these helpers into
|
||||
// send(), attachLiveStream(), replay hydration, and renderMessages().
|
||||
(function(){
|
||||
const ROOT=(typeof window!=='undefined')?window:globalThis;
|
||||
|
||||
const ACTIVITY_EVENT_KINDS=Object.freeze([
|
||||
'process_prose',
|
||||
'reasoning',
|
||||
'tool_started',
|
||||
'tool_updated',
|
||||
'tool_completed',
|
||||
'lifecycle_status',
|
||||
'control_boundary',
|
||||
'terminal_status',
|
||||
]);
|
||||
|
||||
const STATE_LAYERS=Object.freeze([
|
||||
Object.freeze({
|
||||
id:'event_envelope',
|
||||
label:'RuntimeAdapter / run-journal Event Envelope',
|
||||
currentSurface:'event_id, run_id, seq, Last-Event-ID / after_seq',
|
||||
role:'durable_identity',
|
||||
authorityRank:1,
|
||||
anchorPolicy:'Anchor identity and replay dedupe must consume this first.',
|
||||
}),
|
||||
Object.freeze({
|
||||
id:'run_journal',
|
||||
label:'Run journal replay events',
|
||||
currentSurface:'read_run_events(), _replay_run_journal, runtime_journal_snapshot',
|
||||
role:'durable_replay',
|
||||
authorityRank:2,
|
||||
anchorPolicy:'Replay hydration should rebuild activity events from this before caches.',
|
||||
}),
|
||||
Object.freeze({
|
||||
id:'settled_transcript',
|
||||
label:'Server settled transcript messages',
|
||||
currentSurface:'/api/session messages and message metadata',
|
||||
role:'durable_settlement',
|
||||
authorityRank:3,
|
||||
anchorPolicy:'Settlement updates the existing anchor final answer and terminal state.',
|
||||
}),
|
||||
Object.freeze({
|
||||
id:'S.messages',
|
||||
label:'Browser transcript projection',
|
||||
currentSurface:'S.messages consumed by renderMessages()',
|
||||
role:'projection_cache',
|
||||
authorityRank:4,
|
||||
anchorPolicy:'Projection input/output, not a second owner for one assistant turn.',
|
||||
}),
|
||||
Object.freeze({
|
||||
id:'INFLIGHT',
|
||||
label:'Browser in-flight recovery cache',
|
||||
currentSurface:'INFLIGHT[session_id], localStorage persisted in-flight state',
|
||||
role:'recovery_cache',
|
||||
authorityRank:5,
|
||||
anchorPolicy:'Recovery fallback only; must not outrank journal or settled transcript.',
|
||||
}),
|
||||
Object.freeze({
|
||||
id:'stream_closure',
|
||||
label:'attachLiveStream closure-local state',
|
||||
currentSurface:'assistantText, reasoningText, parser targets, live tool state',
|
||||
role:'hot_path_cache',
|
||||
authorityRank:6,
|
||||
anchorPolicy:'Hot-path write buffer; normalize into anchor events as the stream advances.',
|
||||
}),
|
||||
Object.freeze({
|
||||
id:'live_dom',
|
||||
label:'Live DOM / Worklog nodes',
|
||||
currentSurface:'#liveAssistantTurn, tool-card rows, Thinking cards',
|
||||
role:'renderer_output',
|
||||
authorityRank:7,
|
||||
anchorPolicy:'DOM continuity is useful, but DOM is never semantic truth.',
|
||||
}),
|
||||
]);
|
||||
|
||||
const SOURCE_EVENT_CLASSIFICATION=Object.freeze({
|
||||
token:Object.freeze({classification:'activity',kind:'process_prose',source:'sse'}),
|
||||
interim_assistant:Object.freeze({classification:'activity',kind:'process_prose',source:'sse'}),
|
||||
reasoning:Object.freeze({classification:'activity',kind:'reasoning',source:'sse'}),
|
||||
tool:Object.freeze({classification:'activity',kind:'tool_started',source:'sse'}),
|
||||
tool_complete:Object.freeze({classification:'activity',kind:'tool_completed',source:'sse'}),
|
||||
tool_update:Object.freeze({classification:'activity',kind:'tool_updated',source:'future_sse'}),
|
||||
compressing:Object.freeze({classification:'activity',kind:'lifecycle_status',source:'sse'}),
|
||||
compressed:Object.freeze({classification:'activity',kind:'lifecycle_status',source:'sse'}),
|
||||
approval:Object.freeze({classification:'activity',kind:'control_boundary',source:'sse'}),
|
||||
clarify:Object.freeze({classification:'activity',kind:'control_boundary',source:'sse'}),
|
||||
pending_steer_leftover:Object.freeze({classification:'activity',kind:'control_boundary',source:'sse'}),
|
||||
goal_continue:Object.freeze({classification:'activity',kind:'control_boundary',source:'sse'}),
|
||||
artifact_reference:Object.freeze({classification:'artifact',kind:'artifact_reference',source:'derived'}),
|
||||
state_saved:Object.freeze({classification:'side_effect',kind:null,source:'sse'}),
|
||||
usage:Object.freeze({classification:'metadata',kind:null,source:'settlement'}),
|
||||
title:Object.freeze({classification:'metadata',kind:null,source:'settlement'}),
|
||||
done:Object.freeze({classification:'activity',kind:'terminal_status',source:'sse'}),
|
||||
cancel:Object.freeze({classification:'activity',kind:'terminal_status',source:'sse'}),
|
||||
error:Object.freeze({classification:'activity',kind:'terminal_status',source:'sse'}),
|
||||
apperror:Object.freeze({classification:'activity',kind:'terminal_status',source:'sse'}),
|
||||
stream_end:Object.freeze({classification:'transport',kind:null,source:'sse'}),
|
||||
runtime_journal_snapshot:Object.freeze({classification:'metadata',kind:null,source:'session_payload'}),
|
||||
inflight_snapshot:Object.freeze({classification:'metadata',kind:null,source:'browser_storage'}),
|
||||
settled_message:Object.freeze({classification:'metadata',kind:null,source:'session_payload'}),
|
||||
});
|
||||
|
||||
const CLASSIFICATION_ORDER=Object.freeze([
|
||||
'activity',
|
||||
'artifact',
|
||||
'side_effect',
|
||||
'metadata',
|
||||
'transport',
|
||||
'excluded',
|
||||
]);
|
||||
|
||||
function _cleanString(value){
|
||||
return typeof value==='string'?value.trim():'';
|
||||
}
|
||||
|
||||
function assistantTurnAnchorEventDedupeKey(event){
|
||||
if(!event||typeof event!=='object') return '';
|
||||
const eventId=_cleanString(event.event_id);
|
||||
if(eventId) return 'event_id:'+eventId;
|
||||
const runId=_cleanString(event.run_id);
|
||||
const seq=(event.seq!=null&&event.seq!=='')?String(event.seq):'';
|
||||
if(runId&&seq) return 'run_seq:'+runId+':'+seq;
|
||||
const sid=_cleanString(event.session_id);
|
||||
const localId=_cleanString(event.local_id);
|
||||
if(sid&&localId) return 'local:'+sid+':'+localId;
|
||||
return '';
|
||||
}
|
||||
|
||||
function classifyAssistantTurnAnchorSourceEvent(sourceType){
|
||||
const key=_cleanString(sourceType);
|
||||
return SOURCE_EVENT_CLASSIFICATION[key]||Object.freeze({
|
||||
classification:'excluded',
|
||||
kind:null,
|
||||
source:key||'unknown',
|
||||
});
|
||||
}
|
||||
|
||||
function isAssistantTurnAnchorActivityKind(kind){
|
||||
return ACTIVITY_EVENT_KINDS.indexOf(kind)!==-1;
|
||||
}
|
||||
|
||||
function createAssistantTurnAnchorSeed(input){
|
||||
const opts=(input&&typeof input==='object')?input:{};
|
||||
const sessionId=_cleanString(opts.session_id);
|
||||
if(!sessionId) throw new Error('assistant turn anchor requires session_id');
|
||||
const streamId=_cleanString(opts.stream_id);
|
||||
const runId=_cleanString(opts.run_id);
|
||||
const turnId=_cleanString(opts.turn_id)||[
|
||||
'local',
|
||||
sessionId,
|
||||
runId||streamId||'pending',
|
||||
_cleanString(opts.local_id)||'assistant',
|
||||
].join(':');
|
||||
return {
|
||||
identity:{
|
||||
session_id:sessionId,
|
||||
turn_id:turnId,
|
||||
run_id:runId||null,
|
||||
stream_id:streamId||null,
|
||||
source_message_refs:Array.isArray(opts.source_message_refs)?opts.source_message_refs.slice():[],
|
||||
},
|
||||
lifecycle:{
|
||||
status:_cleanString(opts.status)||'created',
|
||||
terminal_state:null,
|
||||
started_at:opts.started_at||null,
|
||||
completed_at:null,
|
||||
},
|
||||
content:{
|
||||
final_answer:'',
|
||||
final_message_ref:null,
|
||||
},
|
||||
activity_events:[],
|
||||
artifacts:[],
|
||||
side_effects:[],
|
||||
usage:null,
|
||||
presentation_state:{
|
||||
compact_worklog:{expanded:false},
|
||||
transparent_stream:{expanded:false},
|
||||
scroll:{follow:true},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ROOT.HermesAssistantTurnAnchors=Object.freeze({
|
||||
version:'phase0',
|
||||
activityEventKinds:ACTIVITY_EVENT_KINDS,
|
||||
stateLayers:STATE_LAYERS,
|
||||
sourceEventClassification:SOURCE_EVENT_CLASSIFICATION,
|
||||
classificationOrder:CLASSIFICATION_ORDER,
|
||||
createAssistantTurnAnchorSeed,
|
||||
assistantTurnAnchorEventDedupeKey,
|
||||
classifyAssistantTurnAnchorSourceEvent,
|
||||
isAssistantTurnAnchorActivityKind,
|
||||
});
|
||||
})();
|
||||
@@ -1498,6 +1498,7 @@
|
||||
<div class="toast" id="toast"></div>
|
||||
<script src="static/i18n.js?v=__WEBUI_VERSION__" defer></script>
|
||||
<script src="static/icons.js?v=__WEBUI_VERSION__" defer></script>
|
||||
<script src="static/assistant_turn_anchors.js?v=__WEBUI_VERSION__" defer></script>
|
||||
<script src="static/ui.js?v=__WEBUI_VERSION__" defer></script>
|
||||
<script src="static/workspace.js?v=__WEBUI_VERSION__" defer></script>
|
||||
<script src="static/terminal.js?v=__WEBUI_VERSION__" defer></script>
|
||||
|
||||
@@ -25,6 +25,7 @@ const SHELL_ASSETS = [
|
||||
'./static/style.css' + VQ,
|
||||
'./static/pwa-startup.js' + VQ,
|
||||
'./static/boot.js' + VQ,
|
||||
'./static/assistant_turn_anchors.js' + VQ,
|
||||
'./static/ui.js' + VQ,
|
||||
'./static/messages.js' + VQ,
|
||||
'./static/sessions.js' + VQ,
|
||||
|
||||
196
tests/test_stable_assistant_turn_anchor_phase0.py
Normal file
196
tests/test_stable_assistant_turn_anchor_phase0.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""Phase 0 contract tests for Stable Assistant Turn Anchors (#3926).
|
||||
|
||||
The first implementation slice is intentionally non-visual. It adds the
|
||||
inventory and pure helper surface that later phases can wire into send(),
|
||||
attachLiveStream(), replay hydration, and renderMessages().
|
||||
"""
|
||||
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"
|
||||
INDEX_HTML = REPO / "static" / "index.html"
|
||||
MESSAGES_JS = REPO / "static" / "messages.js"
|
||||
UI_JS = REPO / "static" / "ui.js"
|
||||
SESSIONS_JS = REPO / "static" / "sessions.js"
|
||||
SW_JS = REPO / "static" / "sw.js"
|
||||
PHASE0_DOC = REPO / "docs" / "architecture" / "stable-assistant-turn-anchor-phase0.md"
|
||||
NODE = shutil.which("node")
|
||||
|
||||
|
||||
def _read(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _anchor_api_snapshot() -> dict:
|
||||
assert NODE, "node is required for assistant_turn_anchors.js helper 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 anchor = api.createAssistantTurnAnchorSeed({{
|
||||
session_id:'sid-1',
|
||||
stream_id:'stream-1',
|
||||
run_id:'run-1',
|
||||
source_message_refs:['m1'],
|
||||
}});
|
||||
const out = {{
|
||||
version: api.version,
|
||||
kinds: api.activityEventKinds,
|
||||
layers: api.stateLayers,
|
||||
classifications: api.sourceEventClassification,
|
||||
classificationOrder: api.classificationOrder,
|
||||
tokenKind: api.classifyAssistantTurnAnchorSourceEvent('token').kind,
|
||||
streamEndClass: api.classifyAssistantTurnAnchorSourceEvent('stream_end').classification,
|
||||
unknownClass: api.classifyAssistantTurnAnchorSourceEvent('unknown_future').classification,
|
||||
eventIdKey: api.assistantTurnAnchorEventDedupeKey({{event_id:'run-1:2', text:'same'}}),
|
||||
runSeqKey: api.assistantTurnAnchorEventDedupeKey({{run_id:'run-1', seq:2, timestamp:123}}),
|
||||
localKey: api.assistantTurnAnchorEventDedupeKey({{session_id:'sid-1', local_id:'local-1', content:'ignored'}}),
|
||||
zeroSeqKey: api.assistantTurnAnchorEventDedupeKey({{run_id:'run-1', seq:0, session_id:'sid-1', local_id:'local-1'}}),
|
||||
nanSeqKey: api.assistantTurnAnchorEventDedupeKey({{run_id:'run-1', seq:NaN, session_id:'sid-1', local_id:'local-1'}}),
|
||||
emptySeqKey: api.assistantTurnAnchorEventDedupeKey({{run_id:'run-1', seq:'', session_id:'sid-1', local_id:'local-1'}}),
|
||||
emptyKey: api.assistantTurnAnchorEventDedupeKey({{content:'visible text only', timestamp:123}}),
|
||||
artifactIsActivityKind: api.isAssistantTurnAnchorActivityKind('artifact_reference'),
|
||||
terminalIsActivityKind: api.isAssistantTurnAnchorActivityKind('terminal_status'),
|
||||
anchor,
|
||||
}};
|
||||
console.log(JSON.stringify(out));
|
||||
"""
|
||||
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_phase0_scaffold_is_loaded_before_current_rendering_modules():
|
||||
html = _read(INDEX_HTML)
|
||||
anchor_pos = html.index('static/assistant_turn_anchors.js?v=__WEBUI_VERSION__')
|
||||
ui_pos = html.index('static/ui.js?v=__WEBUI_VERSION__')
|
||||
sessions_pos = html.index('static/sessions.js?v=__WEBUI_VERSION__')
|
||||
messages_pos = html.index('static/messages.js?v=__WEBUI_VERSION__')
|
||||
|
||||
assert anchor_pos < ui_pos < sessions_pos < messages_pos
|
||||
assert "'./static/assistant_turn_anchors.js' + VQ" in _read(SW_JS)
|
||||
assert "HermesAssistantTurnAnchors" not in _read(UI_JS)
|
||||
assert "HermesAssistantTurnAnchors" not in _read(SESSIONS_JS)
|
||||
assert "HermesAssistantTurnAnchors" not in _read(MESSAGES_JS)
|
||||
|
||||
|
||||
def test_phase0_inventory_names_current_state_layers_in_authority_order():
|
||||
data = _anchor_api_snapshot()
|
||||
layer_ids = [layer["id"] for layer in data["layers"]]
|
||||
assert layer_ids == [
|
||||
"event_envelope",
|
||||
"run_journal",
|
||||
"settled_transcript",
|
||||
"S.messages",
|
||||
"INFLIGHT",
|
||||
"stream_closure",
|
||||
"live_dom",
|
||||
]
|
||||
ranks = [layer["authorityRank"] for layer in data["layers"]]
|
||||
assert ranks == sorted(ranks)
|
||||
assert data["layers"][0]["role"] == "durable_identity"
|
||||
assert data["layers"][-1]["role"] == "renderer_output"
|
||||
|
||||
|
||||
def test_phase0_classifies_all_current_live_to_final_sources():
|
||||
data = _anchor_api_snapshot()
|
||||
classifications = data["classifications"]
|
||||
required = {
|
||||
"token": ("activity", "process_prose"),
|
||||
"interim_assistant": ("activity", "process_prose"),
|
||||
"reasoning": ("activity", "reasoning"),
|
||||
"tool": ("activity", "tool_started"),
|
||||
"tool_complete": ("activity", "tool_completed"),
|
||||
"tool_update": ("activity", "tool_updated"),
|
||||
"compressing": ("activity", "lifecycle_status"),
|
||||
"compressed": ("activity", "lifecycle_status"),
|
||||
"approval": ("activity", "control_boundary"),
|
||||
"clarify": ("activity", "control_boundary"),
|
||||
"pending_steer_leftover": ("activity", "control_boundary"),
|
||||
"goal_continue": ("activity", "control_boundary"),
|
||||
"done": ("activity", "terminal_status"),
|
||||
"cancel": ("activity", "terminal_status"),
|
||||
"error": ("activity", "terminal_status"),
|
||||
"apperror": ("activity", "terminal_status"),
|
||||
"stream_end": ("transport", None),
|
||||
"runtime_journal_snapshot": ("metadata", None),
|
||||
"inflight_snapshot": ("metadata", None),
|
||||
"settled_message": ("metadata", None),
|
||||
}
|
||||
for source, expected in required.items():
|
||||
item = classifications[source]
|
||||
assert (item["classification"], item["kind"]) == expected
|
||||
|
||||
assert data["tokenKind"] == "process_prose"
|
||||
assert data["streamEndClass"] == "transport"
|
||||
assert data["unknownClass"] == "excluded"
|
||||
assert data["artifactIsActivityKind"] is False
|
||||
assert data["terminalIsActivityKind"] is True
|
||||
for item in classifications.values():
|
||||
if item["classification"] == "activity":
|
||||
assert item["kind"] in data["kinds"]
|
||||
elif item["kind"] is not None:
|
||||
assert item["kind"] not in data["kinds"]
|
||||
|
||||
|
||||
def test_phase0_dedupe_prefers_event_envelope_not_visible_text_or_timestamps():
|
||||
data = _anchor_api_snapshot()
|
||||
assert data["eventIdKey"] == "event_id:run-1:2"
|
||||
assert data["runSeqKey"] == "run_seq:run-1:2"
|
||||
assert data["localKey"] == "local:sid-1:local-1"
|
||||
assert data["zeroSeqKey"] == "run_seq:run-1:0"
|
||||
assert data["nanSeqKey"] == "run_seq:run-1:NaN"
|
||||
assert data["emptySeqKey"] == "local:sid-1:local-1"
|
||||
assert data["emptyKey"] == ""
|
||||
|
||||
helper_src = _read(ANCHORS_JS).split("function assistantTurnAnchorEventDedupeKey", 1)[1]
|
||||
helper_src = helper_src.split("function classifyAssistantTurnAnchorSourceEvent", 1)[0]
|
||||
assert "event_id" in helper_src
|
||||
assert "run_id" in helper_src
|
||||
assert "seq" in helper_src
|
||||
assert "text" not in helper_src
|
||||
assert "content" not in helper_src
|
||||
assert "timestamp" not in helper_src
|
||||
assert "created_at" not in helper_src
|
||||
|
||||
|
||||
def test_phase0_anchor_seed_matches_rfc_shape_without_registering_state():
|
||||
data = _anchor_api_snapshot()
|
||||
anchor = data["anchor"]
|
||||
assert anchor["identity"]["session_id"] == "sid-1"
|
||||
assert anchor["identity"]["run_id"] == "run-1"
|
||||
assert anchor["identity"]["stream_id"] == "stream-1"
|
||||
assert anchor["lifecycle"]["status"] == "created"
|
||||
assert anchor["content"]["final_answer"] == ""
|
||||
assert anchor["activity_events"] == []
|
||||
assert anchor["artifacts"] == []
|
||||
assert anchor["side_effects"] == []
|
||||
assert anchor["presentation_state"]["compact_worklog"]["expanded"] is False
|
||||
assert anchor["presentation_state"]["transparent_stream"]["expanded"] is False
|
||||
|
||||
|
||||
def test_phase0_inventory_doc_matches_scaffold_contract():
|
||||
doc = _read(PHASE0_DOC)
|
||||
for marker in [
|
||||
"RuntimeAdapter / run-journal Event Envelope",
|
||||
"Run journal replay events",
|
||||
"Server settled transcript",
|
||||
"`S.messages`",
|
||||
"`INFLIGHT`",
|
||||
"Stream closure state",
|
||||
"Live DOM",
|
||||
"Dedupe Invariant",
|
||||
"`event_id`",
|
||||
"`run_id + seq`",
|
||||
"`session_id + local_id`",
|
||||
]:
|
||||
assert marker in doc
|
||||
Reference in New Issue
Block a user