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 +71 -0
- contextgc/anchors.py +90 -0
- contextgc/client.py +197 -0
- contextgc/gc_engine.py +960 -0
- contextgc/py.typed +0 -0
- contextgc/sanitizer.py +386 -0
- contextgc/schemas/coding.json +55 -0
- contextgc/schemas/logistics.json +46 -0
- contextgc/schemas/travel.json +120 -0
- contextgc/schemas.py +146 -0
- contextgc/state_dag.py +674 -0
- contextgc/state_protocol.py +368 -0
- contextgc/transcript.py +130 -0
- contextgc/value_shapes.py +194 -0
- contextgc/vector_tier.py +110 -0
- contextgc-0.4.1.dist-info/METADATA +1474 -0
- contextgc-0.4.1.dist-info/RECORD +20 -0
- contextgc-0.4.1.dist-info/WHEEL +5 -0
- contextgc-0.4.1.dist-info/licenses/LICENSE +21 -0
- contextgc-0.4.1.dist-info/top_level.txt +1 -0
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
|