feat(profiler/behave_shell): emit motor.paste_burst_rate
BEHAVE-EXTRACTOR.md Phase A Step 3. Same paste-event ratio as
motor.input_modality but coarser-bucketed: this is the *habit*
signal (does the operator reach for paste at all?), where
input_modality is the dominant-channel signal.
* _features/motor.py:paste_burst_rate(ctx) emits one Observation
per session in {none, occasional, habitual} with confidence
0.70 / 0.70 / 0.80.
* Thresholds: PASTE_RATE_OCCASIONAL_MIN=0.10,
PASTE_RATE_HABITUAL_MIN=0.50.
Splits YOU-sim from LW/CLAUDE-FF/CLAUDE-CL — LLM-driven sessions
paste habitually, real humans rarely paste.
Tests: pure-typed → none; 1-paste-in-10 → occasional;
paste-majority → habitual; output-only → no observation; habitual
confidence > occasional confidence.
This commit is contained in:
@@ -11,10 +11,14 @@ from typing import Callable, Iterable
|
|||||||
from decnet_behave_core.spec.envelope import Observation
|
from decnet_behave_core.spec.envelope import Observation
|
||||||
|
|
||||||
from decnet.profiler.behave_shell._ctx import SessionContext
|
from decnet.profiler.behave_shell._ctx import SessionContext
|
||||||
from decnet.profiler.behave_shell._features.motor import input_modality
|
from decnet.profiler.behave_shell._features.motor import (
|
||||||
|
input_modality,
|
||||||
|
paste_burst_rate,
|
||||||
|
)
|
||||||
|
|
||||||
FeatureFn = Callable[[SessionContext], Iterable[Observation]]
|
FeatureFn = Callable[[SessionContext], Iterable[Observation]]
|
||||||
|
|
||||||
FEATURES: tuple[FeatureFn, ...] = (
|
FEATURES: tuple[FeatureFn, ...] = (
|
||||||
input_modality,
|
input_modality,
|
||||||
|
paste_burst_rate,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ from decnet.profiler.behave_shell._features._emit import make_observation
|
|||||||
from decnet.profiler.behave_shell._thresholds import (
|
from decnet.profiler.behave_shell._thresholds import (
|
||||||
MODALITY_PASTED_MIN,
|
MODALITY_PASTED_MIN,
|
||||||
MODALITY_TYPED_MAX,
|
MODALITY_TYPED_MAX,
|
||||||
|
PASTE_RATE_HABITUAL_MIN,
|
||||||
|
PASTE_RATE_OCCASIONAL_MIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -43,3 +45,34 @@ def input_modality(ctx: SessionContext) -> Iterator[Observation]:
|
|||||||
value=modality,
|
value=modality,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def paste_burst_rate(ctx: SessionContext) -> Iterator[Observation]:
|
||||||
|
"""Emit ``motor.paste_burst_rate`` ∈ {none, occasional, habitual}.
|
||||||
|
|
||||||
|
Same paste-event ratio as ``input_modality`` but coarser-bucketed:
|
||||||
|
this primitive is the *habit* signal (does the operator reach for
|
||||||
|
paste at all?), where input_modality is the dominant-channel
|
||||||
|
signal (is the session paste-driven overall?). Splits YOU-sim from
|
||||||
|
LW/CLAUDE-FF/CLAUDE-CL — LLM-driven sessions paste habitually,
|
||||||
|
real humans don't.
|
||||||
|
"""
|
||||||
|
n = len(ctx.input_events)
|
||||||
|
if n == 0:
|
||||||
|
return
|
||||||
|
ratio = ctx.paste_event_count / n
|
||||||
|
if ratio >= PASTE_RATE_HABITUAL_MIN:
|
||||||
|
level = "habitual"
|
||||||
|
confidence = 0.80
|
||||||
|
elif ratio >= PASTE_RATE_OCCASIONAL_MIN:
|
||||||
|
level = "occasional"
|
||||||
|
confidence = 0.70
|
||||||
|
else:
|
||||||
|
level = "none"
|
||||||
|
confidence = 0.70
|
||||||
|
yield make_observation(
|
||||||
|
ctx,
|
||||||
|
primitive="motor.paste_burst_rate",
|
||||||
|
value=level,
|
||||||
|
confidence=confidence,
|
||||||
|
)
|
||||||
|
|||||||
53
tests/profiler/behave_shell/test_motor_paste_burst_rate.py
Normal file
53
tests/profiler/behave_shell/test_motor_paste_burst_rate.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""Step 3: ``motor.paste_burst_rate`` — none / occasional / habitual."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decnet.profiler.behave_shell import extract_session
|
||||||
|
from decnet.profiler.behave_shell._parse import AsciinemaEvent
|
||||||
|
|
||||||
|
|
||||||
|
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_pure_typed_session_emits_none() -> None:
|
||||||
|
events: list[AsciinemaEvent] = [(i * 0.1, "i", c) for i, c in enumerate("ls -la\r")]
|
||||||
|
out = list(extract_session(events, sid="rate-typed"))
|
||||||
|
assert _of(out, "motor.paste_burst_rate").value == "none"
|
||||||
|
|
||||||
|
|
||||||
|
def test_one_paste_in_ten_emits_occasional() -> None:
|
||||||
|
# 1 paste + 9 single-char typed events → ratio 0.10 → occasional
|
||||||
|
events: list[AsciinemaEvent] = [(0.0, "i", "echo paste\r")]
|
||||||
|
events += [(0.5 + i * 0.1, "i", c) for i, c in enumerate("ls -la\rp")]
|
||||||
|
out = list(extract_session(events, sid="rate-occasional"))
|
||||||
|
assert _of(out, "motor.paste_burst_rate").value == "occasional"
|
||||||
|
|
||||||
|
|
||||||
|
def test_paste_majority_emits_habitual() -> None:
|
||||||
|
events: list[AsciinemaEvent] = [
|
||||||
|
(0.0, "i", "echo a\r"),
|
||||||
|
(1.0, "i", "echo b\r"),
|
||||||
|
(2.0, "i", "echo c\r"),
|
||||||
|
(3.0, "i", "x"),
|
||||||
|
]
|
||||||
|
out = list(extract_session(events, sid="rate-habitual"))
|
||||||
|
assert _of(out, "motor.paste_burst_rate").value == "habitual"
|
||||||
|
|
||||||
|
|
||||||
|
def test_zero_input_emits_nothing() -> None:
|
||||||
|
out = list(extract_session([(0.0, "o", "hi\r\n")], sid="rate-empty"))
|
||||||
|
assert [o for o in out if o.primitive == "motor.paste_burst_rate"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_confidence_higher_for_habitual_than_occasional() -> None:
|
||||||
|
pasted = [
|
||||||
|
(0.0, "i", "echo a\r"), (1.0, "i", "echo b\r"), (2.0, "i", "echo c\r"),
|
||||||
|
]
|
||||||
|
occasional = [(0.0, "i", "echo a\r")] + [
|
||||||
|
(0.5 + i * 0.1, "i", c) for i, c in enumerate("ls -la\rps\r")
|
||||||
|
]
|
||||||
|
h = _of(list(extract_session(pasted, sid="conf-h")), "motor.paste_burst_rate")
|
||||||
|
o = _of(list(extract_session(occasional, sid="conf-o")), "motor.paste_burst_rate")
|
||||||
|
assert h.confidence > o.confidence
|
||||||
Reference in New Issue
Block a user