feat(profiler/behave_shell): emit temporal.session_duration
Bucket ctx.duration_s against SESSION_DURATION_SHORT_MAX (60s) / MEDIUM_MAX (600s) / LONG_MAX (3600s); else marathon. Direct measurement, confidence 0.85. Skip emission only when no commands and zero duration. New _features/temporal.py module opens Phase E.
This commit is contained in:
@@ -24,6 +24,9 @@ from decnet.profiler.behave_shell._features.cognitive import (
|
|||||||
inter_command_consistency,
|
inter_command_consistency,
|
||||||
inter_command_latency_class,
|
inter_command_latency_class,
|
||||||
)
|
)
|
||||||
|
from decnet.profiler.behave_shell._features.temporal import (
|
||||||
|
session_duration,
|
||||||
|
)
|
||||||
from decnet.profiler.behave_shell._features.motor import (
|
from decnet.profiler.behave_shell._features.motor import (
|
||||||
command_chunking,
|
command_chunking,
|
||||||
error_correction,
|
error_correction,
|
||||||
@@ -59,4 +62,5 @@ FEATURES: tuple[FeatureFn, ...] = (
|
|||||||
error_resilience_retry_tactic,
|
error_resilience_retry_tactic,
|
||||||
error_resilience_frustration_typing,
|
error_resilience_frustration_typing,
|
||||||
error_resilience_fallback_to_man,
|
error_resilience_fallback_to_man,
|
||||||
|
session_duration,
|
||||||
)
|
)
|
||||||
|
|||||||
50
decnet/profiler/behave_shell/_features/temporal.py
Normal file
50
decnet/profiler/behave_shell/_features/temporal.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"""``temporal.*`` feature functions — per-session subset.
|
||||||
|
|
||||||
|
Phase E ships the four ``temporal.*`` primitives that don't need
|
||||||
|
observation history. The other three (``session_timing``,
|
||||||
|
``persistence``, ``lifecycle_markers.idle_periodicity``) are Tier B
|
||||||
|
and computed by the attribution engine, not the extractor.
|
||||||
|
|
||||||
|
Step E.1: ``temporal.session_duration``.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
|
from decnet_behave_core.spec.envelope import Observation
|
||||||
|
|
||||||
|
from decnet.profiler.behave_shell._ctx import SessionContext
|
||||||
|
from decnet.profiler.behave_shell._features._emit import make_observation
|
||||||
|
from decnet.profiler.behave_shell._thresholds import (
|
||||||
|
SESSION_DURATION_LONG_MAX,
|
||||||
|
SESSION_DURATION_MEDIUM_MAX,
|
||||||
|
SESSION_DURATION_SHORT_MAX,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def session_duration(ctx: SessionContext) -> Iterator[Observation]:
|
||||||
|
"""Emit ``temporal.session_duration`` ∈ {short, medium, long, marathon}.
|
||||||
|
|
||||||
|
Direct measurement off ``ctx.duration_s``. Skip emission only when
|
||||||
|
the session has neither commands nor any duration to speak of —
|
||||||
|
a one-event session with ``duration_s == 0`` and no commands has
|
||||||
|
nothing honest to bucket. Confidence is high — duration is a fact,
|
||||||
|
not an inference.
|
||||||
|
"""
|
||||||
|
if ctx.duration_s <= 0.0 and not ctx.commands:
|
||||||
|
return
|
||||||
|
d = ctx.duration_s
|
||||||
|
if d < SESSION_DURATION_SHORT_MAX:
|
||||||
|
value = "short"
|
||||||
|
elif d < SESSION_DURATION_MEDIUM_MAX:
|
||||||
|
value = "medium"
|
||||||
|
elif d < SESSION_DURATION_LONG_MAX:
|
||||||
|
value = "long"
|
||||||
|
else:
|
||||||
|
value = "marathon"
|
||||||
|
yield make_observation(
|
||||||
|
ctx,
|
||||||
|
primitive="temporal.session_duration",
|
||||||
|
value=value,
|
||||||
|
confidence=0.85,
|
||||||
|
)
|
||||||
@@ -170,6 +170,20 @@ TOOL_VOCAB_BROAD_MIN: int = 10
|
|||||||
FRUSTRATION_LOW_MAX: float = 0.10
|
FRUSTRATION_LOW_MAX: float = 0.10
|
||||||
FRUSTRATION_MODERATE_MAX: float = 0.30
|
FRUSTRATION_MODERATE_MAX: float = 0.30
|
||||||
|
|
||||||
|
# ── temporal.session_duration (Step E.1) ───────────────────────────────────
|
||||||
|
# Bucket edges (seconds) for ``ctx.duration_s``:
|
||||||
|
#
|
||||||
|
# duration_s < SESSION_DURATION_SHORT_MAX → short
|
||||||
|
# duration_s < SESSION_DURATION_MEDIUM_MAX → medium
|
||||||
|
# duration_s < SESSION_DURATION_LONG_MAX → long
|
||||||
|
# else → marathon
|
||||||
|
#
|
||||||
|
# 60s / 600s / 3600s are the BEHAVE-EXTRACTOR.md defaults; D.8-equivalent
|
||||||
|
# re-tune for E lands when calibration corpus is run.
|
||||||
|
SESSION_DURATION_SHORT_MAX: float = 60.0
|
||||||
|
SESSION_DURATION_MEDIUM_MAX: float = 600.0
|
||||||
|
SESSION_DURATION_LONG_MAX: float = 3600.0
|
||||||
|
|
||||||
# ── motor.keystroke_cadence (Step B.1) ──────────────────────────────────────
|
# ── motor.keystroke_cadence (Step B.1) ──────────────────────────────────────
|
||||||
# Typing bursts split at gaps > IKI_THINK_MAX_S so think-pauses between
|
# Typing bursts split at gaps > IKI_THINK_MAX_S so think-pauses between
|
||||||
# commands don't inflate the within-burst CV. Mirrors the prototype's
|
# commands don't inflate the within-burst CV. Mirrors the prototype's
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""Step E.1: ``temporal.session_duration``."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decnet.profiler.behave_shell import extract_session
|
||||||
|
from decnet.profiler.behave_shell._parse import AsciinemaEvent
|
||||||
|
|
||||||
|
|
||||||
|
PRIMITIVE = "temporal.session_duration"
|
||||||
|
|
||||||
|
|
||||||
|
def _of(observations: list, primitive: str):
|
||||||
|
obs = [o for o in observations if o.primitive == primitive]
|
||||||
|
assert len(obs) == 1, f"expected exactly one {primitive}, got {len(obs)}"
|
||||||
|
return obs[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_session_no_emission() -> None:
|
||||||
|
out = list(extract_session([], sid="dur-empty"))
|
||||||
|
assert [o for o in out if o.primitive == PRIMITIVE] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_under_60s_emits_short() -> None:
|
||||||
|
events: list[AsciinemaEvent] = [(0.0, "i", "a"), (30.0, "i", "b")]
|
||||||
|
obs = _of(list(extract_session(events, sid="dur-short")), PRIMITIVE)
|
||||||
|
assert obs.value == "short"
|
||||||
|
|
||||||
|
|
||||||
|
def test_under_600s_emits_medium() -> None:
|
||||||
|
events: list[AsciinemaEvent] = [(0.0, "i", "a"), (300.0, "i", "b")]
|
||||||
|
obs = _of(list(extract_session(events, sid="dur-med")), PRIMITIVE)
|
||||||
|
assert obs.value == "medium"
|
||||||
|
|
||||||
|
|
||||||
|
def test_under_3600s_emits_long() -> None:
|
||||||
|
events: list[AsciinemaEvent] = [(0.0, "i", "a"), (1800.0, "i", "b")]
|
||||||
|
obs = _of(list(extract_session(events, sid="dur-long")), PRIMITIVE)
|
||||||
|
assert obs.value == "long"
|
||||||
|
|
||||||
|
|
||||||
|
def test_over_3600s_emits_marathon() -> None:
|
||||||
|
events: list[AsciinemaEvent] = [(0.0, "i", "a"), (7200.0, "i", "b")]
|
||||||
|
obs = _of(list(extract_session(events, sid="dur-marathon")), PRIMITIVE)
|
||||||
|
assert obs.value == "marathon"
|
||||||
|
|
||||||
|
|
||||||
|
def test_high_confidence() -> None:
|
||||||
|
"""Duration is a fact, not an inference — confidence stays high."""
|
||||||
|
events: list[AsciinemaEvent] = [(0.0, "i", "a"), (30.0, "i", "b")]
|
||||||
|
obs = _of(list(extract_session(events, sid="dur-conf")), PRIMITIVE)
|
||||||
|
assert obs.confidence >= 0.80
|
||||||
Reference in New Issue
Block a user