contextgc 0.4.1__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.
contextgc/__init__.py ADDED
@@ -0,0 +1,71 @@
1
+ """
2
+ contextgc -- a deterministic context compiler for AI agent transcripts.
3
+
4
+ Long agent sessions accumulate superseded assertions and bulky tool payloads.
5
+ The model then has to reconcile two conflicting values for the same key, and
6
+ pays for every token of it. contextgc compiles the transcript down to the
7
+ current facts, retires the ones that were replaced, and compacts tool output --
8
+ deterministically, in microseconds, with no model call and no network.
9
+
10
+ >>> from contextgc import compile_messages
11
+ >>> msgs, telemetry = compile_messages([
12
+ ... {"role": "user", "content": "deliver to Tower B"},
13
+ ... {"role": "user", "content": "actually deliver to Gate 2"},
14
+ ... ])
15
+ >>> telemetry["compression_ratio_pct"] > 0
16
+ True
17
+
18
+ What it does and does not do is documented in the README under "Honest scope".
19
+ """
20
+
21
+ from .anchors import InvariantAuditor, PolicyInvariantAnchor
22
+ from .client import (
23
+ ContextGCEngine,
24
+ compile_messages,
25
+ compile_transcript,
26
+ patch_openai,
27
+ )
28
+ from .sanitizer import ToolSanitizer
29
+ from .schemas import list_schemas, load_schema, schema_summary
30
+ from .state_dag import SOURCE_DECLARED, SOURCE_INFERRED, FactNode, StateDAG
31
+ from .state_protocol import (
32
+ DECLARING_ROLES,
33
+ StateDeclaration,
34
+ escape_value,
35
+ normalise_key,
36
+ parse_declaration,
37
+ render_instruction,
38
+ strip_blocks,
39
+ )
40
+ from .transcript import parse_transcript
41
+ from .vector_tier import RetiredTurnArchive, VectorMemoryTier
42
+
43
+ __version__ = "0.4.0"
44
+
45
+ __all__ = [
46
+ "compile_messages",
47
+ "compile_transcript",
48
+ "patch_openai",
49
+ "ContextGCEngine",
50
+ "StateDAG",
51
+ "FactNode",
52
+ "SOURCE_DECLARED",
53
+ "SOURCE_INFERRED",
54
+ "ToolSanitizer",
55
+ "PolicyInvariantAnchor",
56
+ "InvariantAuditor",
57
+ "parse_transcript",
58
+ "list_schemas",
59
+ "load_schema",
60
+ "schema_summary",
61
+ "StateDeclaration",
62
+ "DECLARING_ROLES",
63
+ "parse_declaration",
64
+ "render_instruction",
65
+ "strip_blocks",
66
+ "normalise_key",
67
+ "escape_value",
68
+ "RetiredTurnArchive",
69
+ "VectorMemoryTier",
70
+ "__version__",
71
+ ]
contextgc/anchors.py ADDED
@@ -0,0 +1,90 @@
1
+ """
2
+ ContextGC: declared invariants, pinned in the compiled context.
3
+
4
+ An *invariant* is a rule the caller declares must survive compilation: a
5
+ refund ceiling, a "never print secrets" guardrail, a regulatory constraint.
6
+ The compiler inlines them into the emitted context and refuses to let tracked
7
+ entity state overwrite them.
8
+
9
+ Two honest limitations, stated here because they were previously papered over
10
+ with a "100% compliance" badge that this module cannot support:
11
+
12
+ 1. Inlining a rule into a prompt makes it *more salient*. It does not make
13
+ compliance *guaranteed*. No prompt-level technique can guarantee that.
14
+ 2. :meth:`InvariantAuditor.scan` is a regular-expression heuristic over text.
15
+ It catches the obvious shapes and misses the rest. It is a cheap tripwire,
16
+ not a proof.
17
+
18
+ If you need actual enforcement, gate the action itself: reject the tool call,
19
+ not the sentence. This module's job is to make the rule present and loud.
20
+ """
21
+
22
+ import re
23
+ from typing import Any, Dict, List, Optional
24
+
25
+
26
+ class PolicyInvariantAnchor:
27
+ """
28
+ Holds caller-declared invariants and renders them into the compiled context.
29
+
30
+ There are no built-in domain rules. Shipping a hardcoded "refunds over
31
+ ₹150 need approval" default meant the tool silently imported one demo
32
+ scenario's business rules into every unrelated session. Declare your own.
33
+ """
34
+
35
+ def __init__(self, invariants: Optional[List[str]] = None):
36
+ self.invariants: List[str] = list(invariants or [])
37
+
38
+ def add(self, rule: str) -> None:
39
+ self.invariants.append(rule)
40
+
41
+ def render_anchor_block(self) -> str:
42
+ """Render the declared invariants as a compact, labelled block."""
43
+ if not self.invariants:
44
+ return ""
45
+ lines = ["[DECLARED_INVARIANTS]"]
46
+ lines.extend(f" - {rule}" for rule in self.invariants)
47
+ return "\n".join(lines)
48
+
49
+ def scan(self, text: str) -> Dict[str, Any]:
50
+ """
51
+ Heuristic lint of text against a small set of generic danger patterns.
52
+
53
+ This is a tripwire, not a guarantee. It knows nothing about your
54
+ business rules -- supply those via :meth:`scan_with` if you need them.
55
+ """
56
+ return InvariantAuditor().scan(text)
57
+
58
+
59
+ class InvariantAuditor:
60
+ """Generic secret-leak / PII tripwire. Replaceable by a caller-supplied checker."""
61
+
62
+ #: Deliberately narrow and generic. Domain policy belongs to the caller.
63
+ PATTERNS = (
64
+ ("private_key", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")),
65
+ ("assigned_secret", re.compile(
66
+ r"\b(?:private[-_ ]?key|secret[-_ ]?key|api[-_ ]?key|access[-_ ]?token)\s*[:=]\s*\S+",
67
+ re.IGNORECASE,
68
+ )),
69
+ ("aws_key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
70
+ )
71
+
72
+ def scan(self, text: str) -> Dict[str, Any]:
73
+ findings = [
74
+ {"rule": name, "match": m.group(0)[:60]}
75
+ for name, pattern in self.PATTERNS
76
+ for m in [pattern.search(text or "")] if m
77
+ ]
78
+ return {"has_violation": bool(findings), "violations": findings}
79
+
80
+ def scan_with(self, text: str, invariants: List[Dict[str, Any]]) -> Dict[str, Any]:
81
+ """
82
+ Apply caller-supplied rules. Each rule is ``{"name": str, "pattern": compiled}``.
83
+ Lets a caller encode its own refund ceilings, allowlists, and so on
84
+ without patching this library.
85
+ """
86
+ findings = []
87
+ for rule in invariants:
88
+ if rule["pattern"].search(text or ""):
89
+ findings.append({"rule": rule["name"], "match": rule["pattern"].search(text).group(0)[:60]})
90
+ return {"has_violation": bool(findings), "violations": findings}
contextgc/client.py ADDED
@@ -0,0 +1,197 @@
1
+ """
2
+ ContextGC: the two things most callers need.
3
+
4
+ from contextgc import compile_messages, compile_transcript
5
+
6
+ ``compile_messages`` OpenAI-format message list -> compiled context + telemetry
7
+ ``compile_transcript`` pasted plain-text chat log -> compiled context + telemetry
8
+
9
+ Both are pure functions: same input, same output, no network, no model calls,
10
+ no global state.
11
+ """
12
+
13
+ import functools
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+
16
+ from .gc_engine import ContextGCEngine
17
+ from .transcript import parse_transcript
18
+
19
+ __all__ = [
20
+ "compile_messages",
21
+ "compile_transcript",
22
+ "patch_openai",
23
+ "ContextGCEngine",
24
+ ]
25
+
26
+
27
+ def compile_messages(
28
+ messages: List[Dict[str, Any]],
29
+ mode: str = "compact",
30
+ invariants: Optional[List[str]] = None,
31
+ recall_query: Optional[str] = None,
32
+ session_id: Optional[str] = None,
33
+ teach_protocol: bool = False,
34
+ schema: Optional[Dict[str, Any]] = None,
35
+ declaration_policy: str = "flag",
36
+ value_policy: str = "flag",
37
+ ) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
38
+ """
39
+ Compile an OpenAI-format message history.
40
+
41
+ Args:
42
+ messages: ``[{"role": ..., "content": ...}, ...]``
43
+ mode: ``"compact"`` (retire superseded turns, inline state at head) or
44
+ ``"cache_friendly"`` (leave the prefix byte-identical, append the
45
+ state register at the tail).
46
+ invariants: rules to pin into the compiled context.
47
+ recall_query: if set, search retired turns and re-inject the best match.
48
+ teach_protocol: prepend the state-protocol instruction so the agent
49
+ declares its own state changes. See :mod:`contextgc.state_protocol`.
50
+ schema: entity patterns to enable. Empty by default -- the read path has
51
+ no built-in domain, because the default it used to ship was measured
52
+ producing nonsense on real transcripts.
53
+ declaration_policy: ``"flag"`` (default) keeps a declared key the schema
54
+ does not define and reports it under ``telemetry.rejected_writes``;
55
+ ``"reject"`` drops it; ``"off"`` disables the check. Only has an
56
+ effect when a ``schema`` is supplied, since without one there is no
57
+ vocabulary to judge a key against.
58
+
59
+ Returns:
60
+ ``(compiled_messages, telemetry)``. Every telemetry field is measured at
61
+ runtime; see :meth:`ContextGCEngine.process_session`.
62
+ """
63
+ engine = ContextGCEngine(
64
+ session_id=session_id or "contextgc", invariants=invariants, schema=schema,
65
+ declaration_policy=declaration_policy, value_policy=value_policy,
66
+ )
67
+ result = engine.process_session(
68
+ messages, query_for_jit=recall_query, mode=mode, teach_protocol=teach_protocol
69
+ )
70
+ return result["cleaned_messages"], result["telemetry"]
71
+
72
+
73
+ def compile_transcript(
74
+ text: str,
75
+ mode: str = "compact",
76
+ invariants: Optional[List[str]] = None,
77
+ recall_query: Optional[str] = None,
78
+ teach_protocol: bool = False,
79
+ schema: Optional[Dict[str, Any]] = None,
80
+ declaration_policy: str = "flag",
81
+ value_policy: str = "flag",
82
+ ) -> Tuple[List[Dict[str, Any]], Dict[str, Any], List[str]]:
83
+ """
84
+ Compile a pasted plain-text transcript.
85
+
86
+ Accepts either JSON (an OpenAI message array) or the line-oriented form::
87
+
88
+ user: deliver to Tower B, Flat 402. Severe peanut allergy.
89
+ assistant: Confirmed.
90
+ tool: TOOL_OUTPUT [inventory] {"items": [...]}
91
+
92
+ Returns ``(compiled_messages, telemetry, parse_warnings)``.
93
+ """
94
+ messages, warnings = parse_transcript(text)
95
+ if not messages:
96
+ return [], {
97
+ "error": "no messages parsed",
98
+ "parse_warnings": warnings,
99
+ }, warnings
100
+
101
+ compiled, telemetry = compile_messages(
102
+ messages, mode=mode, invariants=invariants, recall_query=recall_query,
103
+ teach_protocol=teach_protocol, schema=schema,
104
+ declaration_policy=declaration_policy, value_policy=value_policy,
105
+ )
106
+ return compiled, telemetry, warnings
107
+
108
+
109
+ def patch_openai(
110
+ client: Any,
111
+ mode: str = "compact",
112
+ invariants: Optional[List[str]] = None,
113
+ teach_protocol: bool = False,
114
+ schema: Optional[Dict[str, Any]] = None,
115
+ session_id: Optional[str] = None,
116
+ declaration_policy: str = "flag",
117
+ value_policy: str = "flag",
118
+ ) -> Any:
119
+ """
120
+ Wrap ``client.chat.completions.create`` so outgoing message histories are
121
+ compiled first. The response carries the telemetry as ``.context_gc``.
122
+
123
+ Args:
124
+ schema: entity patterns to enable, exactly as for
125
+ :func:`compile_messages`. This parameter was missing, which meant
126
+ that with the default schema now empty there was **no way to turn
127
+ state tracking on through this wrapper** -- the most documented
128
+ integration path silently compiled with nothing enabled. A whole
129
+ schema file may be passed; ``{"entities": ...}`` and ``_comment``
130
+ are handled for you.
131
+ session_id: namespaces the retired-turn archive and the recall tier.
132
+ Omitted before, so two wrapped clients in one process shared the
133
+ default session and could surface each other's retired turns.
134
+ declaration_policy: what to do with a declared key the schema does not
135
+ define. See :func:`compile_messages`. Added here for the same reason
136
+ ``schema`` had to be: this is the most documented integration path, so
137
+ a policy that is unreachable from it is a policy nobody will set.
138
+
139
+ Set ``teach_protocol=True`` to have the agent declare its own state changes.
140
+ The declared facts are authoritative and carry provenance, which is what
141
+ lets the compiler retire a superseded turn without stranding a value the
142
+ turn uniquely held.
143
+
144
+ Works with both ``openai.OpenAI`` and ``openai.AsyncOpenAI``.
145
+
146
+ .. warning::
147
+ This rewrites the messages actually sent upstream. Compile in a dry run
148
+ first (``compile_messages``) and read ``telemetry`` before you trust it
149
+ on a live agent -- the state tracker is regex-based and will miss
150
+ assertions it cannot pattern-match. See the README's Limitations section.
151
+ """
152
+ original_create = client.chat.completions.create
153
+
154
+ def _compile(messages):
155
+ return compile_messages(
156
+ messages,
157
+ mode=mode,
158
+ invariants=invariants,
159
+ teach_protocol=teach_protocol,
160
+ schema=schema,
161
+ session_id=session_id,
162
+ declaration_policy=declaration_policy,
163
+ value_policy=value_policy,
164
+ )
165
+
166
+ @functools.wraps(original_create)
167
+ def wrapped_create(*args, **kwargs):
168
+ messages = kwargs.get("messages")
169
+ if messages:
170
+ kwargs["messages"], telemetry = _compile(messages)
171
+ response = original_create(*args, **kwargs)
172
+ try:
173
+ response.context_gc = telemetry
174
+ except Exception:
175
+ pass
176
+ return response
177
+ return original_create(*args, **kwargs)
178
+
179
+ @functools.wraps(original_create)
180
+ async def async_wrapped_create(*args, **kwargs):
181
+ messages = kwargs.get("messages")
182
+ if messages:
183
+ kwargs["messages"], telemetry = _compile(messages)
184
+ response = await original_create(*args, **kwargs)
185
+ try:
186
+ response.context_gc = telemetry
187
+ except Exception:
188
+ pass
189
+ return response
190
+ return await original_create(*args, **kwargs)
191
+
192
+ import inspect
193
+ if inspect.iscoroutinefunction(original_create):
194
+ client.chat.completions.create = async_wrapped_create
195
+ else:
196
+ client.chat.completions.create = wrapped_create
197
+ return client