zeno-cli 0.3.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zeno_adapters/__init__.py +17 -0
- zeno_adapters/_common.py +38 -0
- zeno_adapters/anthropic.py +68 -0
- zeno_adapters/claude_code.py +101 -0
- zeno_adapters/crewai.py +92 -0
- zeno_adapters/langgraph.py +49 -0
- zeno_adapters/openai.py +108 -0
- zeno_cli/__init__.py +1 -0
- zeno_cli/_hooks/cc_bridge.py +1016 -0
- zeno_cli/doctor.py +535 -0
- zeno_cli/hook_install.py +269 -0
- zeno_cli/hud/__init__.py +1 -0
- zeno_cli/hud/hud_install.py +652 -0
- zeno_cli/hud/zeno_attention.py +288 -0
- zeno_cli/hud/zeno_cognition.py +457 -0
- zeno_cli/hud/zeno_hud.py +496 -0
- zeno_cli/interview_invites.py +342 -0
- zeno_cli/login.py +241 -0
- zeno_cli/main.py +2534 -0
- zeno_cli/onboard.py +206 -0
- zeno_cli/outreach.py +456 -0
- zeno_cli/version.py +67 -0
- zeno_cli-0.3.4.dist-info/METADATA +161 -0
- zeno_cli-0.3.4.dist-info/RECORD +69 -0
- zeno_cli-0.3.4.dist-info/WHEEL +4 -0
- zeno_cli-0.3.4.dist-info/entry_points.txt +4 -0
- zeno_core/__init__.py +67 -0
- zeno_core/analytics.py +193 -0
- zeno_core/rtlx_s.py +460 -0
- zeno_core/streak.py +178 -0
- zeno_core/tlx_s.py +192 -0
- zeno_sdk/__init__.py +6 -0
- zeno_sdk/_generated/__init__.py +6 -0
- zeno_sdk/_generated/client.py +819 -0
- zeno_sdk/_migrations/alembic/env.py +33 -0
- zeno_sdk/_migrations/alembic/script.py.mako +18 -0
- zeno_sdk/_migrations/alembic/versions/0001_initial.py +79 -0
- zeno_sdk/_migrations/alembic/versions/0002_cognition_samples.py +53 -0
- zeno_sdk/_migrations/alembic/versions/0003_cognition_drivers.py +41 -0
- zeno_sdk/_migrations/alembic/versions/0004_transcript_intelligence.py +248 -0
- zeno_sdk/_migrations/alembic.ini +35 -0
- zeno_sdk/_runtime.py +12 -0
- zeno_sdk/adapters/__init__.py +15 -0
- zeno_sdk/adapters/anthropic.py +5 -0
- zeno_sdk/adapters/claude_code.py +5 -0
- zeno_sdk/adapters/crewai.py +5 -0
- zeno_sdk/adapters/langgraph.py +5 -0
- zeno_sdk/adapters/openai.py +5 -0
- zeno_sdk/auth.py +25 -0
- zeno_sdk/client.py +87 -0
- zeno_sdk/config.py +61 -0
- zeno_sdk/daemon.py +72 -0
- zeno_sdk/privacy.py +46 -0
- zeno_sdk/session.py +179 -0
- zeno_sdk/storage.py +487 -0
- zeno_sdk/types/__init__.py +121 -0
- zeno_session_intel/__init__.py +19 -0
- zeno_session_intel/analytics.py +588 -0
- zeno_session_intel/compression.py +123 -0
- zeno_session_intel/ingest.py +376 -0
- zeno_session_intel/model.py +129 -0
- zeno_session_intel/parsers/__init__.py +31 -0
- zeno_session_intel/parsers/claude_code.py +169 -0
- zeno_session_intel/parsers/codex.py +265 -0
- zeno_session_intel/parsers/cursor.py +198 -0
- zeno_session_intel/prices.py +281 -0
- zeno_session_intel/schema.py +277 -0
- zeno_session_intel/signals.py +319 -0
- zeno_session_intel/taxonomy.py +71 -0
zeno_sdk/client.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ._runtime import run_coro
|
|
10
|
+
from .config import ZenoConfig, load_config
|
|
11
|
+
from .daemon import DaemonManager
|
|
12
|
+
from .privacy import redact_metadata
|
|
13
|
+
from .session import SessionContext
|
|
14
|
+
from .storage import ZenoStorage
|
|
15
|
+
from .types import SupervisionEvent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class Zeno:
|
|
20
|
+
project: str
|
|
21
|
+
project_root: Path | None = None
|
|
22
|
+
config: ZenoConfig | None = None
|
|
23
|
+
_storage: ZenoStorage | None = None
|
|
24
|
+
_daemon: DaemonManager | None = None
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
self.config = self.config or load_config(self.project_root)
|
|
28
|
+
self._storage = ZenoStorage(self.config.db_path)
|
|
29
|
+
self._daemon = DaemonManager(
|
|
30
|
+
db_path=self.config.db_path,
|
|
31
|
+
flush_interval_seconds=self.config.flush_interval_seconds,
|
|
32
|
+
)
|
|
33
|
+
run_coro(self._storage.migrate())
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def storage(self) -> ZenoStorage:
|
|
37
|
+
if self._storage is None:
|
|
38
|
+
raise RuntimeError("Storage is not initialized.")
|
|
39
|
+
return self._storage
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def daemon(self) -> DaemonManager:
|
|
43
|
+
if self._daemon is None:
|
|
44
|
+
raise RuntimeError("Daemon is not initialized.")
|
|
45
|
+
return self._daemon
|
|
46
|
+
|
|
47
|
+
def _project_id(self) -> uuid.UUID:
|
|
48
|
+
return uuid.uuid5(uuid.NAMESPACE_URL, self.project)
|
|
49
|
+
|
|
50
|
+
def session(self, *, harness: str) -> SessionContext:
|
|
51
|
+
self.daemon.start()
|
|
52
|
+
return SessionContext(
|
|
53
|
+
storage=self.storage,
|
|
54
|
+
project_id=self._project_id(),
|
|
55
|
+
harness=harness,
|
|
56
|
+
probes_enabled=self.config.probes_enabled,
|
|
57
|
+
record_content=self.config.record_content,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def record_event(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
session_id: uuid.UUID,
|
|
64
|
+
type: str,
|
|
65
|
+
agent_run_id: uuid.UUID | None = None,
|
|
66
|
+
latency_ms_to_decide: int | None = None,
|
|
67
|
+
metadata: dict[str, Any] | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
self.daemon.start()
|
|
70
|
+
event = SupervisionEvent(
|
|
71
|
+
id=uuid.uuid4(),
|
|
72
|
+
session_id=session_id,
|
|
73
|
+
agent_run_id=agent_run_id,
|
|
74
|
+
type=type,
|
|
75
|
+
timestamp=datetime.now(tz=UTC),
|
|
76
|
+
latency_ms_to_decide=latency_ms_to_decide,
|
|
77
|
+
metadata=redact_metadata(metadata, record_content=self.config.record_content),
|
|
78
|
+
)
|
|
79
|
+
run_coro(self.storage.insert_event(event))
|
|
80
|
+
|
|
81
|
+
def prompt_load_probe(
|
|
82
|
+
self, *, session: SessionContext, subscales: dict[str, int] | None, skipped: bool = False
|
|
83
|
+
) -> None:
|
|
84
|
+
session.prompt_load_probe(subscales=subscales, skipped=skipped)
|
|
85
|
+
|
|
86
|
+
def shutdown(self) -> None:
|
|
87
|
+
self.daemon.stop()
|
zeno_sdk/config.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tomllib
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class ZenoConfig:
|
|
12
|
+
base_dir: Path
|
|
13
|
+
db_path: Path
|
|
14
|
+
probes_enabled: bool
|
|
15
|
+
flush_interval_seconds: int
|
|
16
|
+
redaction_enabled: bool
|
|
17
|
+
record_content: bool
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _read_toml(path: Path) -> dict[str, Any]:
|
|
21
|
+
if not path.exists():
|
|
22
|
+
return {}
|
|
23
|
+
return tomllib.loads(path.read_text(encoding="utf-8"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_config(project_root: Path | None = None) -> ZenoConfig:
|
|
27
|
+
home = Path.home()
|
|
28
|
+
base_dir = Path(os.getenv("ZENO_HOME", home / ".zeno")).expanduser()
|
|
29
|
+
base_dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
user_config = _read_toml(base_dir / "config.toml")
|
|
32
|
+
project_config: dict[str, Any] = {}
|
|
33
|
+
if project_root is not None:
|
|
34
|
+
project_config = _read_toml(project_root / ".zeno.toml")
|
|
35
|
+
|
|
36
|
+
cfg = {}
|
|
37
|
+
cfg.update(user_config.get("zeno", {}))
|
|
38
|
+
cfg.update(project_config.get("zeno", {}))
|
|
39
|
+
|
|
40
|
+
probes_enabled = str(
|
|
41
|
+
os.getenv("ZENO_PROBES_ENABLED", cfg.get("probes_enabled", "true"))
|
|
42
|
+
).lower()
|
|
43
|
+
redaction_enabled = str(
|
|
44
|
+
os.getenv("ZENO_REDACTION_ENABLED", cfg.get("redaction_enabled", "true"))
|
|
45
|
+
).lower()
|
|
46
|
+
record_content = str(
|
|
47
|
+
os.getenv("ZENO_RECORD_CONTENT", cfg.get("record_content", "false"))
|
|
48
|
+
).lower()
|
|
49
|
+
flush_interval = int(
|
|
50
|
+
os.getenv("ZENO_FLUSH_INTERVAL_SECONDS", cfg.get("flush_interval_seconds", 5))
|
|
51
|
+
)
|
|
52
|
+
db_path = Path(os.getenv("ZENO_DB_PATH", str(base_dir / "zeno.db"))).expanduser()
|
|
53
|
+
|
|
54
|
+
return ZenoConfig(
|
|
55
|
+
base_dir=base_dir,
|
|
56
|
+
db_path=db_path,
|
|
57
|
+
probes_enabled=probes_enabled in {"1", "true", "yes", "on"},
|
|
58
|
+
flush_interval_seconds=max(flush_interval, 1),
|
|
59
|
+
redaction_enabled=redaction_enabled in {"1", "true", "yes", "on"},
|
|
60
|
+
record_content=record_content in {"1", "true", "yes", "on"},
|
|
61
|
+
)
|
zeno_sdk/daemon.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import multiprocessing as mp
|
|
4
|
+
import signal
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ._runtime import run_coro
|
|
11
|
+
from .storage import ZenoStorage, make_tax_point
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _daemon_main(db_path: str, flush_interval: int, stop_event: mp.Event) -> None:
|
|
15
|
+
running = True
|
|
16
|
+
|
|
17
|
+
def _handle_sigterm(_signum: int, _frame: object) -> None:
|
|
18
|
+
nonlocal running
|
|
19
|
+
running = False
|
|
20
|
+
|
|
21
|
+
signal.signal(signal.SIGTERM, _handle_sigterm)
|
|
22
|
+
storage = ZenoStorage(Path(db_path))
|
|
23
|
+
while running and not stop_event.is_set():
|
|
24
|
+
# Keep process alive and ready for future sync/aggregation jobs.
|
|
25
|
+
# Current v1 loop only runs periodic liveness cycles.
|
|
26
|
+
time.sleep(max(flush_interval, 1))
|
|
27
|
+
_ = storage
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class DaemonManager:
|
|
32
|
+
db_path: Path
|
|
33
|
+
flush_interval_seconds: int
|
|
34
|
+
_process: mp.Process | None = None
|
|
35
|
+
_stop_event: mp.Event | None = None
|
|
36
|
+
|
|
37
|
+
def start(self) -> None:
|
|
38
|
+
if self._process and self._process.is_alive():
|
|
39
|
+
return
|
|
40
|
+
self._stop_event = mp.Event()
|
|
41
|
+
self._process = mp.Process(
|
|
42
|
+
target=_daemon_main,
|
|
43
|
+
args=(str(self.db_path), self.flush_interval_seconds, self._stop_event),
|
|
44
|
+
daemon=True,
|
|
45
|
+
)
|
|
46
|
+
self._process.start()
|
|
47
|
+
|
|
48
|
+
def stop(self, timeout_seconds: float = 5.0) -> None:
|
|
49
|
+
if not self._process:
|
|
50
|
+
return
|
|
51
|
+
if self._stop_event:
|
|
52
|
+
self._stop_event.set()
|
|
53
|
+
self._process.join(timeout_seconds)
|
|
54
|
+
if self._process.is_alive():
|
|
55
|
+
self._process.terminate()
|
|
56
|
+
self._process.join(timeout_seconds)
|
|
57
|
+
self._process = None
|
|
58
|
+
self._stop_event = None
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def pid(self) -> int | None:
|
|
62
|
+
if self._process and self._process.is_alive():
|
|
63
|
+
return self._process.pid
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def compute_incremental_tax_point(storage: ZenoStorage, session_id: str) -> None:
|
|
68
|
+
stats = run_coro(storage.compute_session_stats(session_id))
|
|
69
|
+
point = make_tax_point(session_id, stats)
|
|
70
|
+
# Use stable deterministic id to keep updates idempotent.
|
|
71
|
+
point.id = uuid.uuid5(uuid.UUID(session_id), "tax-point")
|
|
72
|
+
run_coro(storage.upsert_tax_point(point))
|
zeno_sdk/privacy.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
BLOCKED_KEYS = {"prompt", "prompt_text", "output", "output_text", "completion", "response"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def hash_text(value: str) -> str:
|
|
12
|
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def redact_metadata(
|
|
16
|
+
metadata: Mapping[str, Any] | None,
|
|
17
|
+
*,
|
|
18
|
+
record_content: bool = False,
|
|
19
|
+
record_tool_args: bool = False,
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
if metadata is None:
|
|
22
|
+
return {}
|
|
23
|
+
|
|
24
|
+
redacted: dict[str, Any] = {}
|
|
25
|
+
for key, value in metadata.items():
|
|
26
|
+
lowered = key.lower()
|
|
27
|
+
if lowered in BLOCKED_KEYS and not record_content:
|
|
28
|
+
text = str(value)
|
|
29
|
+
redacted[f"{key}_hash"] = hash_text(text)
|
|
30
|
+
redacted[f"{key}_len"] = len(text)
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
if lowered in {"tool_args", "arguments"} and not record_tool_args:
|
|
34
|
+
if isinstance(value, Mapping):
|
|
35
|
+
redacted["tool_arg_keys"] = sorted(value.keys())
|
|
36
|
+
else:
|
|
37
|
+
redacted["tool_arg_keys"] = []
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
41
|
+
redacted[key] = value
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
# Keep complex values serializable and compact.
|
|
45
|
+
redacted[key] = json.loads(json.dumps(value, default=str))
|
|
46
|
+
return redacted
|
zeno_sdk/session.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from zeno_core.tlx_s import run_tlx_survey_tui, should_prompt_probe
|
|
9
|
+
|
|
10
|
+
from ._runtime import run_coro
|
|
11
|
+
from .daemon import compute_incremental_tax_point
|
|
12
|
+
from .privacy import redact_metadata
|
|
13
|
+
from .storage import ZenoStorage
|
|
14
|
+
from .types import AgentRun, LoadProbe, LoadProbeSubscales, Session, SupervisionEvent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _now() -> datetime:
|
|
18
|
+
return datetime.now(tz=UTC)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_probe_state: dict[str, Any] = {
|
|
22
|
+
"sessions_since_last_probe": 0,
|
|
23
|
+
"last_probe_at": None,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(slots=True)
|
|
28
|
+
class AgentRunContext:
|
|
29
|
+
storage: ZenoStorage
|
|
30
|
+
session_id: uuid.UUID
|
|
31
|
+
harness: str
|
|
32
|
+
model: str
|
|
33
|
+
run_id: uuid.UUID
|
|
34
|
+
_outcome: str = "unknown"
|
|
35
|
+
_started_at: datetime | None = None
|
|
36
|
+
_ended_at: datetime | None = None
|
|
37
|
+
|
|
38
|
+
def outcome(self, value: str) -> None:
|
|
39
|
+
self._outcome = value
|
|
40
|
+
|
|
41
|
+
def __enter__(self) -> AgentRunContext:
|
|
42
|
+
self._started_at = _now()
|
|
43
|
+
run = AgentRun(
|
|
44
|
+
id=self.run_id,
|
|
45
|
+
session_id=self.session_id,
|
|
46
|
+
harness=self.harness,
|
|
47
|
+
model=self.model,
|
|
48
|
+
started_at=self._started_at,
|
|
49
|
+
ended_at=None,
|
|
50
|
+
outcome="unknown",
|
|
51
|
+
)
|
|
52
|
+
run_coro(self.storage.insert_agent_run(run))
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __exit__(self, _exc_type: object, _exc: object, _tb: object) -> None:
|
|
56
|
+
self._ended_at = _now()
|
|
57
|
+
if self._started_at is None:
|
|
58
|
+
self._started_at = self._ended_at
|
|
59
|
+
final = AgentRun(
|
|
60
|
+
id=self.run_id,
|
|
61
|
+
session_id=self.session_id,
|
|
62
|
+
harness=self.harness,
|
|
63
|
+
model=self.model,
|
|
64
|
+
started_at=self._started_at,
|
|
65
|
+
ended_at=self._ended_at,
|
|
66
|
+
outcome=self._outcome,
|
|
67
|
+
)
|
|
68
|
+
run_coro(self.storage.insert_agent_run(final))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(slots=True)
|
|
72
|
+
class SessionContext:
|
|
73
|
+
storage: ZenoStorage
|
|
74
|
+
project_id: uuid.UUID
|
|
75
|
+
harness: str
|
|
76
|
+
probes_enabled: bool
|
|
77
|
+
record_content: bool
|
|
78
|
+
session_id: uuid.UUID | None = None
|
|
79
|
+
_started_at: datetime | None = None
|
|
80
|
+
|
|
81
|
+
def __enter__(self) -> SessionContext:
|
|
82
|
+
self.session_id = uuid.uuid4()
|
|
83
|
+
self._started_at = _now()
|
|
84
|
+
session = Session(
|
|
85
|
+
id=self.session_id,
|
|
86
|
+
start_at=self._started_at,
|
|
87
|
+
end_at=None,
|
|
88
|
+
agent_count_max=1,
|
|
89
|
+
harness=self.harness,
|
|
90
|
+
project_id=self.project_id,
|
|
91
|
+
)
|
|
92
|
+
run_coro(self.storage.insert_session(session))
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
def __exit__(self, _exc_type: object, _exc: object, _tb: object) -> None:
|
|
96
|
+
if self.session_id is None or self._started_at is None:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
stats = run_coro(self.storage.compute_session_stats(str(self.session_id)))
|
|
100
|
+
finished = Session(
|
|
101
|
+
id=self.session_id,
|
|
102
|
+
start_at=self._started_at,
|
|
103
|
+
end_at=_now(),
|
|
104
|
+
agent_count_max=max(1, int(round(stats.avg_n_agents)) or 1),
|
|
105
|
+
harness=self.harness,
|
|
106
|
+
project_id=self.project_id,
|
|
107
|
+
)
|
|
108
|
+
run_coro(self.storage.insert_session(finished))
|
|
109
|
+
compute_incremental_tax_point(self.storage, str(self.session_id))
|
|
110
|
+
_probe_state["sessions_since_last_probe"] += 1
|
|
111
|
+
last_probe_at = _probe_state.get("last_probe_at")
|
|
112
|
+
seconds_since = None
|
|
113
|
+
if isinstance(last_probe_at, datetime):
|
|
114
|
+
seconds_since = (_now() - last_probe_at).total_seconds()
|
|
115
|
+
should_prompt = should_prompt_probe(
|
|
116
|
+
session_duration_minutes=(finished.end_at - finished.start_at).total_seconds() / 60,
|
|
117
|
+
sessions_since_last_probe=int(_probe_state["sessions_since_last_probe"]),
|
|
118
|
+
probes_enabled=self.probes_enabled,
|
|
119
|
+
seconds_since_last_probe=seconds_since,
|
|
120
|
+
active_agent_runs=0,
|
|
121
|
+
)
|
|
122
|
+
if should_prompt:
|
|
123
|
+
result = run_tlx_survey_tui()
|
|
124
|
+
self.prompt_load_probe(result.subscales, skipped=result.skipped)
|
|
125
|
+
if result.comment_hash:
|
|
126
|
+
self.record(type="intervene", metadata={"tlx_comment_hash": result.comment_hash})
|
|
127
|
+
_probe_state["sessions_since_last_probe"] = 0
|
|
128
|
+
_probe_state["last_probe_at"] = _now()
|
|
129
|
+
|
|
130
|
+
def agent_run(self, *, model: str) -> AgentRunContext:
|
|
131
|
+
if self.session_id is None:
|
|
132
|
+
raise RuntimeError("Session is not active.")
|
|
133
|
+
return AgentRunContext(
|
|
134
|
+
storage=self.storage,
|
|
135
|
+
session_id=self.session_id,
|
|
136
|
+
harness=self.harness,
|
|
137
|
+
model=model,
|
|
138
|
+
run_id=uuid.uuid4(),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def record(
|
|
142
|
+
self,
|
|
143
|
+
*,
|
|
144
|
+
type: str,
|
|
145
|
+
agent_run_id: uuid.UUID | None = None,
|
|
146
|
+
latency_ms_to_decide: int | None = None,
|
|
147
|
+
metadata: dict[str, Any] | None = None,
|
|
148
|
+
**extra_metadata: Any,
|
|
149
|
+
) -> None:
|
|
150
|
+
if self.session_id is None:
|
|
151
|
+
raise RuntimeError("Session is not active.")
|
|
152
|
+
merged_metadata = dict(metadata or {})
|
|
153
|
+
merged_metadata.update(extra_metadata)
|
|
154
|
+
event = SupervisionEvent(
|
|
155
|
+
id=uuid.uuid4(),
|
|
156
|
+
session_id=self.session_id,
|
|
157
|
+
agent_run_id=agent_run_id,
|
|
158
|
+
type=type,
|
|
159
|
+
timestamp=_now(),
|
|
160
|
+
latency_ms_to_decide=latency_ms_to_decide,
|
|
161
|
+
metadata=redact_metadata(merged_metadata, record_content=self.record_content),
|
|
162
|
+
)
|
|
163
|
+
run_coro(self.storage.insert_event(event))
|
|
164
|
+
|
|
165
|
+
def prompt_load_probe(self, subscales: dict[str, int] | None, skipped: bool = False) -> None:
|
|
166
|
+
if self.session_id is None:
|
|
167
|
+
raise RuntimeError("Session is not active.")
|
|
168
|
+
if not self.probes_enabled and not skipped:
|
|
169
|
+
return
|
|
170
|
+
prompted = _now()
|
|
171
|
+
probe = LoadProbe(
|
|
172
|
+
id=uuid.uuid4(),
|
|
173
|
+
session_id=self.session_id,
|
|
174
|
+
prompted_at=prompted,
|
|
175
|
+
responded_at=None if skipped else prompted,
|
|
176
|
+
skipped=skipped,
|
|
177
|
+
subscales=LoadProbeSubscales(**subscales) if subscales is not None else None,
|
|
178
|
+
)
|
|
179
|
+
run_coro(self.storage.insert_load_probe(probe))
|