agentcassette 0.1.0__py3-none-win_amd64.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.
@@ -0,0 +1,24 @@
1
+ """Python runtime hooks for AgentCassette."""
2
+
3
+ from .openai_hook import (
4
+ OpenAIHookError,
5
+ OpenAIReplayDivergenceError,
6
+ OpenAIReplayError,
7
+ record_agent_step,
8
+ recording_openai,
9
+ recording_tool,
10
+ replaying_openai,
11
+ )
12
+ from .openai_session import openai_client, openai_session
13
+
14
+ __all__ = [
15
+ "OpenAIHookError",
16
+ "OpenAIReplayDivergenceError",
17
+ "OpenAIReplayError",
18
+ "openai_client",
19
+ "openai_session",
20
+ "record_agent_step",
21
+ "recording_openai",
22
+ "recording_tool",
23
+ "replaying_openai",
24
+ ]
@@ -0,0 +1,169 @@
1
+ """Minimal JSONL cassette writer for Python runtime hooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Mapping
9
+
10
+ from .hashing import hash_value
11
+ from .privacy import PrivacyMode, sanitize_event
12
+
13
+ SCHEMA_VERSION = "0.1"
14
+
15
+ ALLOWED_EVENTS = {
16
+ "trace.start",
17
+ "llm.call",
18
+ "llm.response",
19
+ "tool.call",
20
+ "tool.response",
21
+ "retrieval.call",
22
+ "retrieval.response",
23
+ "agent.step",
24
+ "error",
25
+ "trace.end",
26
+ }
27
+
28
+
29
+ class CassetteWriter:
30
+ """Write compact, flushed JSONL cassette events with a stable schema."""
31
+
32
+ def __init__(
33
+ self,
34
+ path: str | os.PathLike[str],
35
+ *,
36
+ privacy: PrivacyMode = "safe",
37
+ sanitizer: Callable[[Any], Any] | None = None,
38
+ ) -> None:
39
+ self.path = Path(path)
40
+ self.privacy = privacy
41
+ self.sanitizer = sanitizer
42
+ self._file = None
43
+
44
+ def __enter__(self) -> "CassetteWriter":
45
+ return self.open()
46
+
47
+ def __exit__(self, exc_type, exc, tb) -> None:
48
+ self.close()
49
+
50
+ def open(self) -> "CassetteWriter":
51
+ if self._file is not None:
52
+ return self
53
+
54
+ self.path.parent.mkdir(parents=True, exist_ok=True)
55
+ fd = os.open(
56
+ self.path,
57
+ os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
58
+ 0o600,
59
+ )
60
+ self._file = os.fdopen(fd, "w", encoding="utf-8", newline="\n")
61
+ return self
62
+
63
+ def write_event(self, fields: Mapping[str, Any]) -> dict[str, Any]:
64
+ if self._file is None:
65
+ raise RuntimeError("cassette writer is not open")
66
+
67
+ event = sanitize_event(fields, mode=self.privacy, sanitizer=self.sanitizer)
68
+ event.setdefault("schema_version", SCHEMA_VERSION)
69
+ _refresh_hash_fields(event, privacy=self.privacy)
70
+ _validate_event(event)
71
+
72
+ json.dump(
73
+ event,
74
+ self._file,
75
+ sort_keys=True,
76
+ separators=(",", ":"),
77
+ ensure_ascii=False,
78
+ allow_nan=False,
79
+ )
80
+ self._file.write("\n")
81
+ self._file.flush()
82
+ return event
83
+
84
+ def close(self) -> None:
85
+ if self._file is None:
86
+ return
87
+ file_obj = self._file
88
+ self._file = None
89
+ file_obj.close()
90
+
91
+
92
+ def _validate_event(event: Mapping[str, Any]) -> None:
93
+ version = event.get("schema_version")
94
+ if version != SCHEMA_VERSION:
95
+ raise ValueError(f"unsupported schema_version {version!r}")
96
+
97
+ event_type = event.get("event")
98
+ if not isinstance(event_type, str) or not event_type:
99
+ raise ValueError("event must be a non-empty string")
100
+ if event_type not in ALLOWED_EVENTS:
101
+ raise ValueError(f"unknown event type {event_type!r}")
102
+
103
+ if event_type == "trace.start":
104
+ _require_string(event, "trace_id")
105
+ _require_string(event, "name")
106
+ elif event_type == "llm.call":
107
+ _require_string(event, "span_id")
108
+ _require_string(event, "provider")
109
+ _require_string(event, "model")
110
+ _require_string(event, "input_hash")
111
+ elif event_type == "llm.response":
112
+ _require_string(event, "span_id")
113
+ _require_any(event, ("output", "output_hash", "error"))
114
+ elif event_type == "tool.call":
115
+ _require_string(event, "span_id")
116
+ _require_string(event, "name")
117
+ elif event_type == "tool.response":
118
+ _require_string(event, "span_id")
119
+ _require_any(event, ("output", "error"))
120
+ elif event_type == "retrieval.call":
121
+ _require_string(event, "span_id")
122
+ _require_any(event, ("query", "input_hash"))
123
+ elif event_type == "retrieval.response":
124
+ _require_string(event, "span_id")
125
+ _require_any(event, ("documents", "output_hash"))
126
+ elif event_type == "agent.step":
127
+ _require_string(event, "name")
128
+ elif event_type == "error":
129
+ _require_string(event, "message")
130
+ elif event_type == "trace.end":
131
+ _require_string(event, "trace_id")
132
+ _require_string(event, "status")
133
+
134
+
135
+ def _refresh_hash_fields(event: dict[str, Any], *, privacy: PrivacyMode) -> None:
136
+ if privacy == "hide_all":
137
+ for field in ("output_hash",):
138
+ event.pop(field, None)
139
+ return
140
+
141
+ if "input" in event:
142
+ event["input_hash"] = hash_value(event["input"])
143
+ if "output" in event:
144
+ event["output_hash"] = hash_value(event["output"])
145
+ if "documents" in event:
146
+ event["output_hash"] = hash_value(event["documents"])
147
+
148
+
149
+ def _require_string(event: Mapping[str, Any], field: str) -> None:
150
+ value = event.get(field)
151
+ if not isinstance(value, str) or not value:
152
+ raise ValueError(f"{field} must be a non-empty string")
153
+
154
+
155
+ def _require_any(event: Mapping[str, Any], fields: tuple[str, ...]) -> None:
156
+ for field in fields:
157
+ if field not in event:
158
+ continue
159
+ value = event[field]
160
+ if field == "documents":
161
+ if isinstance(value, list):
162
+ return
163
+ raise ValueError("documents must be an array")
164
+ if value is None:
165
+ raise ValueError(f"{field} must not be null")
166
+ if isinstance(value, str) and not value:
167
+ raise ValueError(f"{field} must not be empty")
168
+ return
169
+ raise ValueError(f"missing one of: {fields}")
@@ -0,0 +1,29 @@
1
+ """Launcher for the AgentCassette Go CLI bundled in the Python package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from importlib.resources import files
9
+
10
+
11
+ def binary_path() -> os.PathLike[str]:
12
+ name = "agentcassette.exe" if os.name == "nt" else "agentcassette"
13
+ return files("agentcassette").joinpath("bin", name)
14
+
15
+
16
+ def main() -> None:
17
+ binary = binary_path()
18
+ if not binary.is_file():
19
+ raise SystemExit(
20
+ "AgentCassette CLI binary is missing from this installation. "
21
+ "Install an official platform wheel instead of a source distribution."
22
+ )
23
+
24
+ result = subprocess.run([os.fspath(binary), *sys.argv[1:]], check=False)
25
+ raise SystemExit(result.returncode)
26
+
27
+
28
+ if __name__ == "__main__":
29
+ main()
@@ -0,0 +1,47 @@
1
+ """Canonical JSON hashing compatible with the Go cassette package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from typing import Any
8
+
9
+ HASH_PREFIX = "sha256:"
10
+
11
+ _GO_JSON_ESCAPES = {
12
+ ord("<"): "\\u003c",
13
+ ord(">"): "\\u003e",
14
+ ord("&"): "\\u0026",
15
+ ord("\u2028"): "\\u2028",
16
+ ord("\u2029"): "\\u2029",
17
+ }
18
+
19
+
20
+ def canonical_json(value: Any) -> str:
21
+ """Return compact, key-sorted JSON for a JSON-serializable value."""
22
+
23
+ try:
24
+ encoded = json.dumps(
25
+ value,
26
+ sort_keys=True,
27
+ separators=(",", ":"),
28
+ ensure_ascii=False,
29
+ allow_nan=False,
30
+ )
31
+ return encoded.translate(_GO_JSON_ESCAPES)
32
+ except (TypeError, ValueError) as exc:
33
+ raise ValueError(f"canonicalize JSON for hash: {exc}") from exc
34
+
35
+
36
+ def hash_value(value: Any) -> str:
37
+ canonical = canonical_json(value)
38
+ digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
39
+ return f"{HASH_PREFIX}{digest}"
40
+
41
+
42
+ def hash_json(raw: str | bytes | bytearray) -> str:
43
+ try:
44
+ value = json.loads(raw)
45
+ except json.JSONDecodeError as exc:
46
+ raise ValueError(f"decode JSON for hash: {exc}") from exc
47
+ return hash_value(value)