grok-loop-kit 1.0.0__tar.gz

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,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: grok-loop-kit
3
+ Version: 1.0.0
4
+ Summary: Auto-apply Grok 4.5 context compaction to xAI Responses API agent loops.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/Booyaka101/grok-loop-kit
7
+ Project-URL: Repository, https://github.com/Booyaka101/grok-loop-kit
8
+ Project-URL: Issues, https://github.com/Booyaka101/grok-loop-kit/issues
9
+ Keywords: grok,grok-4.5,xai,context-compaction,agent,responses-api,llm
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Provides-Extra: requests
13
+ Requires-Dist: requests>=2.28; extra == "requests"
14
+
15
+ # grok-loop-kit (Python)
16
+
17
+ Automatic Grok 4.5 context compaction for xAI Responses API agent loops.
18
+
19
+ ```python
20
+ from grok_loop_kit import GrokLoopClient
21
+
22
+ client = GrokLoopClient(api_key, compact_every=8, compact_at_tokens=8000, model="grok-4.5")
23
+ res = client.send_message("hello")
24
+ print(res["_grokLoopKit"].totalCompactions)
25
+ ```
26
+
27
+ Zero runtime dependencies (stdlib `urllib`). See the repository root README for the
28
+ full API and the Node/LangGraph packages.
@@ -0,0 +1,14 @@
1
+ # grok-loop-kit (Python)
2
+
3
+ Automatic Grok 4.5 context compaction for xAI Responses API agent loops.
4
+
5
+ ```python
6
+ from grok_loop_kit import GrokLoopClient
7
+
8
+ client = GrokLoopClient(api_key, compact_every=8, compact_at_tokens=8000, model="grok-4.5")
9
+ res = client.send_message("hello")
10
+ print(res["_grokLoopKit"].totalCompactions)
11
+ ```
12
+
13
+ Zero runtime dependencies (stdlib `urllib`). See the repository root README for the
14
+ full API and the Node/LangGraph packages.
@@ -0,0 +1,25 @@
1
+ """grok-loop-kit: automatic Grok 4.5 context compaction for xAI Responses API agent loops."""
2
+
3
+ from .async_client import AsyncGrokLoopClient
4
+ from .client import (
5
+ CompactionEvent,
6
+ CompletionWithMeta,
7
+ GrokLoopClient,
8
+ GrokLoopKitMeta,
9
+ build_compacted_transcript,
10
+ extract_assistant_text,
11
+ extract_compaction_text,
12
+ )
13
+
14
+ __all__ = [
15
+ "GrokLoopClient",
16
+ "AsyncGrokLoopClient",
17
+ "CompletionWithMeta",
18
+ "CompactionEvent",
19
+ "GrokLoopKitMeta",
20
+ "build_compacted_transcript",
21
+ "extract_assistant_text",
22
+ "extract_compaction_text",
23
+ ]
24
+
25
+ __version__ = "1.0.0"
@@ -0,0 +1,88 @@
1
+ """
2
+ AsyncGrokLoopClient — an asyncio-friendly facade over GrokLoopClient.
3
+
4
+ Zero extra dependencies: each network-bound call runs the synchronous client in
5
+ a worker thread via ``asyncio.to_thread``, so it never blocks the event loop.
6
+ State (messages, turn_count, compaction counters) lives on the wrapped sync
7
+ client and is exposed here as read-through properties.
8
+
9
+ Note: like the sync client, a single instance is not safe to drive with
10
+ concurrent overlapping calls — its transcript is shared mutable state. Use one
11
+ client per conversation.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ from typing import Any, Callable, Dict, List, Optional
18
+
19
+ from .client import CompletionWithMeta, GrokLoopClient
20
+
21
+
22
+ class AsyncGrokLoopClient:
23
+ def __init__(self, api_key: str, **kwargs: Any) -> None:
24
+ self._c = GrokLoopClient(api_key, **kwargs)
25
+
26
+ # --- async API ---
27
+
28
+ async def send_message(
29
+ self, user_content: str, tools: Optional[List[Dict[str, Any]]] = None
30
+ ) -> CompletionWithMeta:
31
+ return await asyncio.to_thread(self._c.send_message, user_content, tools)
32
+
33
+ async def send_tool_outputs(
34
+ self, outputs: List[Dict[str, str]], tools: Optional[List[Dict[str, Any]]] = None
35
+ ) -> CompletionWithMeta:
36
+ return await asyncio.to_thread(self._c.send_tool_outputs, outputs, tools)
37
+
38
+ async def stream_message(
39
+ self,
40
+ user_content: str,
41
+ tools: Optional[List[Dict[str, Any]]] = None,
42
+ on_token: Optional[Callable[[str], None]] = None,
43
+ ) -> CompletionWithMeta:
44
+ # The sync generator yields tokens as they arrive; on_token fires from the
45
+ # worker thread in real time. Returns the assembled final completion.
46
+ return await asyncio.to_thread(self._c.stream_message, user_content, tools, on_token)
47
+
48
+ async def compact(self) -> Dict[str, Any]:
49
+ return await asyncio.to_thread(self._c.compact)
50
+
51
+ # --- passthroughs ---
52
+
53
+ def get_tool_calls(self, completion: Dict[str, Any]) -> List[Dict[str, Any]]:
54
+ return self._c.get_tool_calls(completion)
55
+
56
+ def reset(self) -> None:
57
+ self._c.reset()
58
+
59
+ def get_state(self) -> Dict[str, Any]:
60
+ return self._c.get_state()
61
+
62
+ def load_state(self, state: Dict[str, Any]) -> None:
63
+ self._c.load_state(state)
64
+
65
+ @property
66
+ def messages(self) -> List[Dict[str, Any]]:
67
+ return self._c.messages
68
+
69
+ @property
70
+ def turn_count(self) -> int:
71
+ return self._c.turn_count
72
+
73
+ @property
74
+ def total_compactions(self) -> int:
75
+ return self._c.total_compactions
76
+
77
+ @property
78
+ def accumulated_tokens(self) -> int:
79
+ return self._c.accumulated_tokens
80
+
81
+ @property
82
+ def estimated_tokens_saved(self) -> int:
83
+ return self._c.estimated_tokens_saved
84
+
85
+ @property
86
+ def sync(self) -> GrokLoopClient:
87
+ """Access the underlying synchronous client."""
88
+ return self._c
@@ -0,0 +1,450 @@
1
+ """
2
+ GrokLoopClient — a thin wrapper over xAI's Responses API that transparently
3
+ applies Grok 4.5 context compaction inside an agent loop.
4
+
5
+ Mirrors the TypeScript implementation. Uses only the standard library
6
+ (urllib) so it has no runtime dependencies.
7
+
8
+ Docs:
9
+ https://docs.x.ai/developers/grok-4-5
10
+ https://docs.x.ai/developers/advanced-api-usage/context-compaction
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import time
17
+ import urllib.error
18
+ import urllib.request
19
+ from dataclasses import dataclass
20
+ from typing import Any, Callable, Dict, List, Optional
21
+
22
+ InputItem = Dict[str, Any]
23
+ Tool = Dict[str, Any]
24
+
25
+ DEFAULT_COMPACT_EVERY = 8
26
+ DEFAULT_COMPACT_AT_TOKENS = 8000
27
+ DEFAULT_MODEL = "grok-4.5"
28
+ DEFAULT_BASE_URL = "https://api.x.ai/v1"
29
+ DEFAULT_MAX_RETRIES = 2
30
+ DEFAULT_RETRY_BASE_MS = 500
31
+ RETRYABLE_STATUS = {429, 500, 502, 503, 504}
32
+
33
+
34
+ @dataclass
35
+ class GrokLoopKitMeta:
36
+ """Bookkeeping attached to every completion GrokLoopClient returns."""
37
+
38
+ turnsSinceCompact: int
39
+ estimatedTokensSaved: int
40
+ totalCompactions: int
41
+
42
+ def as_dict(self) -> Dict[str, int]:
43
+ return {
44
+ "turnsSinceCompact": self.turnsSinceCompact,
45
+ "estimatedTokensSaved": self.estimatedTokensSaved,
46
+ "totalCompactions": self.totalCompactions,
47
+ }
48
+
49
+
50
+ @dataclass
51
+ class CompactionEvent:
52
+ """Details passed to the ``on_compact`` hook each time a compaction runs."""
53
+
54
+ totalCompactions: int
55
+ estimatedTokensSaved: int
56
+ droppedMessageCount: int
57
+ atTurn: int
58
+ compaction: Dict[str, Any]
59
+
60
+
61
+ @dataclass
62
+ class CompletionWithMeta:
63
+ """A raw Responses API completion plus grok-loop-kit bookkeeping."""
64
+
65
+ raw: Dict[str, Any]
66
+ _grokLoopKit: GrokLoopKitMeta
67
+
68
+ @property
69
+ def output(self) -> Any:
70
+ return self.raw.get("output")
71
+
72
+ @property
73
+ def usage(self) -> Dict[str, Any]:
74
+ return self.raw.get("usage", {})
75
+
76
+ def tool_calls(self) -> List[Dict[str, Any]]:
77
+ return [i for i in (self.raw.get("output") or []) if i.get("type") == "function_call"]
78
+
79
+ def __getitem__(self, key: str) -> Any:
80
+ if key == "_grokLoopKit":
81
+ return self._grokLoopKit
82
+ return self.raw[key]
83
+
84
+
85
+ # A pluggable HTTP POST returning (parsed_json). Signature:
86
+ # (base_url, path, body, api_key, headers, timeout) -> dict
87
+ Poster = Callable[..., Dict[str, Any]]
88
+
89
+
90
+ class _RetryableHTTP(Exception):
91
+ def __init__(self, status: int, retry_after: Optional[str], detail: str):
92
+ super().__init__(detail)
93
+ self.status = status
94
+ self.retry_after = retry_after
95
+ self.detail = detail
96
+
97
+
98
+ def _default_post(base_url, path, body, api_key, headers=None, timeout=None):
99
+ data = json.dumps(body).encode("utf-8")
100
+ hdrs = {"content-type": "application/json", "authorization": f"Bearer {api_key}"}
101
+ if headers:
102
+ hdrs.update(headers)
103
+ req = urllib.request.Request(f"{base_url}{path}", data=data, method="POST", headers=hdrs)
104
+ try:
105
+ with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
106
+ return json.loads(resp.read().decode("utf-8"))
107
+ except urllib.error.HTTPError as exc:
108
+ detail = exc.read().decode("utf-8", "replace")[:500]
109
+ if exc.code in RETRYABLE_STATUS:
110
+ raise _RetryableHTTP(exc.code, exc.headers.get("retry-after"), detail) from exc
111
+ raise RuntimeError(f"grok-loop-kit: POST {path} failed {exc.code}: {detail}") from exc
112
+
113
+
114
+ def _default_stream(base_url, path, body, api_key, headers=None, timeout=None):
115
+ """POST with stream:true and yield parsed SSE event dicts as they arrive."""
116
+ data = json.dumps({**body, "stream": True}).encode("utf-8")
117
+ hdrs = {
118
+ "content-type": "application/json",
119
+ "accept": "text/event-stream",
120
+ "authorization": f"Bearer {api_key}",
121
+ }
122
+ if headers:
123
+ hdrs.update(headers)
124
+ req = urllib.request.Request(f"{base_url}{path}", data=data, method="POST", headers=hdrs)
125
+ resp = urllib.request.urlopen(req, timeout=timeout) # noqa: S310
126
+ try:
127
+ for raw in resp:
128
+ line = raw.decode("utf-8").strip()
129
+ if not line.startswith("data:"):
130
+ continue
131
+ payload = line[5:].strip()
132
+ if not payload or payload == "[DONE]":
133
+ continue
134
+ try:
135
+ yield json.loads(payload)
136
+ except json.JSONDecodeError:
137
+ continue
138
+ finally:
139
+ resp.close()
140
+
141
+
142
+ class GrokLoopClient:
143
+ """Auto-compacting client for xAI Responses API agent loops."""
144
+
145
+ def __init__(
146
+ self,
147
+ api_key: str,
148
+ *,
149
+ compact_every: int = DEFAULT_COMPACT_EVERY,
150
+ compact_at_tokens: int = DEFAULT_COMPACT_AT_TOKENS,
151
+ model: str = DEFAULT_MODEL,
152
+ base_url: str = DEFAULT_BASE_URL,
153
+ instructions: Optional[str] = None,
154
+ headers: Optional[Dict[str, str]] = None,
155
+ timeout: Optional[float] = None,
156
+ max_retries: int = DEFAULT_MAX_RETRIES,
157
+ retry_base_ms: int = DEFAULT_RETRY_BASE_MS,
158
+ on_compact: Optional[Callable[[CompactionEvent], None]] = None,
159
+ extra_body: Optional[Dict[str, Any]] = None,
160
+ poster: Optional[Poster] = None,
161
+ stream_poster: Optional[Callable[..., Any]] = None,
162
+ ) -> None:
163
+ if not api_key:
164
+ raise ValueError("grok-loop-kit: api_key is required")
165
+ if compact_every < 1:
166
+ raise ValueError("grok-loop-kit: compact_every must be >= 1")
167
+
168
+ self.api_key = api_key
169
+ self.compact_every = compact_every
170
+ self.compact_at_tokens = compact_at_tokens
171
+ self.model = model
172
+ self.base_url = base_url.rstrip("/")
173
+ self.instructions = instructions
174
+ self.headers = headers or {}
175
+ self.timeout = timeout
176
+ self.max_retries = max_retries
177
+ self.retry_base_ms = retry_base_ms
178
+ self.on_compact = on_compact
179
+ self.extra_body = extra_body or {}
180
+ self._post_impl: Poster = poster or _default_post
181
+ self._stream_impl = stream_poster or _default_stream
182
+
183
+ # State.
184
+ self.messages: List[InputItem] = []
185
+ self.turn_count = 0
186
+ self.accumulated_tokens = 0
187
+ self.turns_since_compact = 0
188
+ self.total_compactions = 0
189
+ self.estimated_tokens_saved = 0
190
+ self._last_input_tokens = 0
191
+
192
+ def send_message(
193
+ self, user_content: str, tools: Optional[List[Tool]] = None
194
+ ) -> CompletionWithMeta:
195
+ """Send a user turn through the loop, auto-compacting when due."""
196
+ self.messages.append({"role": "user", "content": user_content})
197
+ return self._run_turn(tools)
198
+
199
+ def send_tool_outputs(
200
+ self, outputs: List[Dict[str, str]], tools: Optional[List[Tool]] = None
201
+ ) -> CompletionWithMeta:
202
+ """Submit tool-call results and continue the loop (counts as a turn)."""
203
+ for o in outputs:
204
+ self.messages.append(
205
+ {"type": "function_call_output", "call_id": o["call_id"], "output": o["output"]}
206
+ )
207
+ return self._run_turn(tools)
208
+
209
+ def stream_message(
210
+ self,
211
+ user_content: str,
212
+ tools: Optional[List[Tool]] = None,
213
+ on_token: Optional[Callable[[str], None]] = None,
214
+ ) -> CompletionWithMeta:
215
+ """Stream a user turn: deltas go to ``on_token``; returns the final completion."""
216
+ self.messages.append({"role": "user", "content": user_content})
217
+ self.turn_count += 1
218
+ self.turns_since_compact += 1
219
+
220
+ body: Dict[str, Any] = {"model": self.model, "input": self.messages}
221
+ if self.instructions:
222
+ body["instructions"] = self.instructions
223
+ if tools:
224
+ body["tools"] = tools
225
+ body.update(self.extra_body)
226
+
227
+ text_parts: List[str] = []
228
+ final: Optional[Dict[str, Any]] = None
229
+ for ev in self._stream_impl(
230
+ self.base_url, "/responses", body, self.api_key, self.headers, self.timeout
231
+ ):
232
+ t = ev.get("type", "")
233
+ if t.endswith("output_text.delta") and isinstance(ev.get("delta"), str):
234
+ text_parts.append(ev["delta"])
235
+ if on_token:
236
+ on_token(ev["delta"])
237
+ elif t == "response.completed" and ev.get("response"):
238
+ final = ev["response"]
239
+
240
+ completion = final or {
241
+ "object": "response",
242
+ "status": "completed",
243
+ "model": self.model,
244
+ "output": [
245
+ {"type": "message", "role": "assistant",
246
+ "content": [{"type": "output_text", "text": "".join(text_parts)}]}
247
+ ],
248
+ "usage": {},
249
+ }
250
+ return self._finalize_turn(completion)
251
+
252
+ def get_tool_calls(self, completion: Dict[str, Any]) -> List[Dict[str, Any]]:
253
+ """Extract ``function_call`` items from a completion's output."""
254
+ return [i for i in (completion.get("output") or []) if i.get("type") == "function_call"]
255
+
256
+ def reset(self) -> None:
257
+ """Clear all loop state (transcript and counters), keeping configuration."""
258
+ self.messages = []
259
+ self.turn_count = 0
260
+ self.accumulated_tokens = 0
261
+ self.turns_since_compact = 0
262
+ self.total_compactions = 0
263
+ self.estimated_tokens_saved = 0
264
+ self._last_input_tokens = 0
265
+
266
+ def get_state(self) -> Dict[str, Any]:
267
+ """Snapshot the loop state for persistence (JSON-serializable)."""
268
+ return {
269
+ "version": 1,
270
+ "messages": self.messages,
271
+ "turn_count": self.turn_count,
272
+ "accumulated_tokens": self.accumulated_tokens,
273
+ "turns_since_compact": self.turns_since_compact,
274
+ "total_compactions": self.total_compactions,
275
+ "estimated_tokens_saved": self.estimated_tokens_saved,
276
+ "last_input_tokens": self._last_input_tokens,
277
+ }
278
+
279
+ def load_state(self, state: Dict[str, Any]) -> None:
280
+ """Restore loop state produced by ``get_state()``."""
281
+ if state.get("version") != 1:
282
+ raise ValueError(f"grok-loop-kit: unsupported state version {state.get('version')}")
283
+ self.messages = state.get("messages", [])
284
+ self.turn_count = state.get("turn_count", 0)
285
+ self.accumulated_tokens = state.get("accumulated_tokens", 0)
286
+ self.turns_since_compact = state.get("turns_since_compact", 0)
287
+ self.total_compactions = state.get("total_compactions", 0)
288
+ self.estimated_tokens_saved = state.get("estimated_tokens_saved", 0)
289
+ self._last_input_tokens = state.get("last_input_tokens", 0)
290
+
291
+ def compact(self) -> Dict[str, Any]:
292
+ """Fold the current transcript into a compaction item."""
293
+ compaction = self._post_compact(self.messages)
294
+ usage = compaction.get("usage") or {}
295
+ dropped = usage.get("dropped_message_count", len(self.messages))
296
+
297
+ # Per xAI docs: usage.input_tokens = pre-compaction conversation tokens,
298
+ # usage.output_tokens = compacted record size. Removed = input - output.
299
+ pre_tokens = usage.get("input_tokens", self._last_input_tokens)
300
+ compacted_record_tokens = usage.get("output_tokens", 0)
301
+ self.estimated_tokens_saved += max(0, pre_tokens - compacted_record_tokens)
302
+ # Reuse the compaction result the way the API expects: verbatim items.
303
+ self.messages = build_compacted_transcript(compaction)
304
+ self.accumulated_tokens = 0
305
+ self.turns_since_compact = 0
306
+ self.total_compactions += 1
307
+
308
+ if self.on_compact:
309
+ self.on_compact(
310
+ CompactionEvent(
311
+ totalCompactions=self.total_compactions,
312
+ estimatedTokensSaved=self.estimated_tokens_saved,
313
+ droppedMessageCount=dropped,
314
+ atTurn=self.turn_count,
315
+ compaction=compaction,
316
+ )
317
+ )
318
+ return compaction
319
+
320
+ # --- internal ---
321
+
322
+ def _run_turn(self, tools: Optional[List[Tool]]) -> CompletionWithMeta:
323
+ self.turn_count += 1
324
+ self.turns_since_compact += 1
325
+ completion = self._post_responses(self.messages, tools)
326
+ return self._finalize_turn(completion)
327
+
328
+ def _finalize_turn(self, completion: Dict[str, Any]) -> CompletionWithMeta:
329
+ """Shared post-completion bookkeeping: preserve output, tally tokens, compact."""
330
+ # Preserve ALL output items verbatim (messages, reasoning, function_calls).
331
+ output = completion.get("output")
332
+ if isinstance(output, list) and output:
333
+ self.messages.extend(output)
334
+
335
+ # accumulated_tokens tracks the CURRENT rendered context size (what the
336
+ # next request would send), per xAI's "rendered context above a threshold"
337
+ # guidance — not a running sum. Resets to 0 when compaction shrinks context.
338
+ usage = completion.get("usage") or {}
339
+ in_tok = usage.get("input_tokens")
340
+ if isinstance(in_tok, (int, float)):
341
+ self._last_input_tokens = int(in_tok)
342
+ self.accumulated_tokens = int(in_tok) + int(usage.get("output_tokens", 0))
343
+
344
+ if (
345
+ self.turn_count % self.compact_every == 0
346
+ or self.accumulated_tokens > self.compact_at_tokens
347
+ ):
348
+ self.compact()
349
+
350
+ return CompletionWithMeta(
351
+ raw=completion,
352
+ _grokLoopKit=GrokLoopKitMeta(
353
+ turnsSinceCompact=self.turns_since_compact,
354
+ estimatedTokensSaved=self.estimated_tokens_saved,
355
+ totalCompactions=self.total_compactions,
356
+ ),
357
+ )
358
+
359
+ def _post_responses(
360
+ self, input_items: List[InputItem], tools: Optional[List[Tool]]
361
+ ) -> Dict[str, Any]:
362
+ body: Dict[str, Any] = {"model": self.model, "input": input_items}
363
+ if self.instructions:
364
+ body["instructions"] = self.instructions
365
+ if tools:
366
+ body["tools"] = tools
367
+ body.update(self.extra_body)
368
+ return self._post("/responses", body)
369
+
370
+ def _post_compact(self, input_items: List[InputItem]) -> Dict[str, Any]:
371
+ return self._post("/responses/compact", {"model": self.model, "input": input_items})
372
+
373
+ def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
374
+ attempt = 0
375
+ while True:
376
+ try:
377
+ return self._post_impl(
378
+ self.base_url, path, body, self.api_key, self.headers, self.timeout
379
+ )
380
+ except _RetryableHTTP as exc:
381
+ if attempt >= self.max_retries:
382
+ raise RuntimeError(
383
+ f"grok-loop-kit: POST {path} failed {exc.status}: {exc.detail}"
384
+ ) from exc
385
+ time.sleep(self._backoff_s(attempt, exc.retry_after))
386
+ attempt += 1
387
+ except (urllib.error.URLError, TimeoutError):
388
+ if attempt >= self.max_retries:
389
+ raise
390
+ time.sleep(self._backoff_s(attempt, None))
391
+ attempt += 1
392
+
393
+ def _backoff_s(self, attempt: int, retry_after: Optional[str]) -> float:
394
+ if retry_after is not None:
395
+ try:
396
+ return max(0.0, float(retry_after))
397
+ except ValueError:
398
+ pass
399
+ return (self.retry_base_ms * (2 ** attempt)) / 1000.0
400
+
401
+
402
+ def extract_assistant_text(completion: Dict[str, Any]) -> str:
403
+ """Pull the assistant's text out of a Responses API completion."""
404
+ parts: List[str] = []
405
+ for item in completion.get("output") or []:
406
+ if item.get("type") != "message":
407
+ continue
408
+ for c in item.get("content") or []:
409
+ text = c.get("text")
410
+ if isinstance(text, str):
411
+ parts.append(text)
412
+ elif c.get("type") == "refusal" and isinstance(c.get("refusal"), str):
413
+ parts.append(c["refusal"])
414
+ return "".join(parts)
415
+
416
+
417
+ def extract_compaction_text(compaction: Dict[str, Any]) -> str:
418
+ """
419
+ Pull the compacted text out of a compaction reply. The real API returns an
420
+ array of items with ``encrypted_content``; mocks/gateways may return a str.
421
+ """
422
+ out = compaction.get("output")
423
+ if isinstance(out, str):
424
+ return out
425
+ if isinstance(out, list):
426
+ parts: List[str] = []
427
+ for item in out:
428
+ enc = item.get("encrypted_content")
429
+ if isinstance(enc, str):
430
+ parts.append(enc)
431
+ for c in item.get("content") or []:
432
+ text = c.get("text")
433
+ if isinstance(text, str):
434
+ parts.append(text)
435
+ if parts:
436
+ return "".join(parts)
437
+ return "[COMPACTED]"
438
+
439
+
440
+ def build_compacted_transcript(compaction: Dict[str, Any]) -> List[InputItem]:
441
+ """
442
+ Build the post-compaction transcript. Per xAI docs, compaction ``output``
443
+ items are spread back into ``input`` verbatim. Gateways/mocks that return a
444
+ plain string become a single user message.
445
+ """
446
+ out = compaction.get("output")
447
+ if isinstance(out, list) and out:
448
+ return list(out)
449
+ text = out if isinstance(out, str) else extract_compaction_text(compaction)
450
+ return [{"role": "user", "content": text}]
File without changes
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: grok-loop-kit
3
+ Version: 1.0.0
4
+ Summary: Auto-apply Grok 4.5 context compaction to xAI Responses API agent loops.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/Booyaka101/grok-loop-kit
7
+ Project-URL: Repository, https://github.com/Booyaka101/grok-loop-kit
8
+ Project-URL: Issues, https://github.com/Booyaka101/grok-loop-kit/issues
9
+ Keywords: grok,grok-4.5,xai,context-compaction,agent,responses-api,llm
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Provides-Extra: requests
13
+ Requires-Dist: requests>=2.28; extra == "requests"
14
+
15
+ # grok-loop-kit (Python)
16
+
17
+ Automatic Grok 4.5 context compaction for xAI Responses API agent loops.
18
+
19
+ ```python
20
+ from grok_loop_kit import GrokLoopClient
21
+
22
+ client = GrokLoopClient(api_key, compact_every=8, compact_at_tokens=8000, model="grok-4.5")
23
+ res = client.send_message("hello")
24
+ print(res["_grokLoopKit"].totalCompactions)
25
+ ```
26
+
27
+ Zero runtime dependencies (stdlib `urllib`). See the repository root README for the
28
+ full API and the Node/LangGraph packages.
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ grok_loop_kit/__init__.py
4
+ grok_loop_kit/async_client.py
5
+ grok_loop_kit/client.py
6
+ grok_loop_kit/py.typed
7
+ grok_loop_kit.egg-info/PKG-INFO
8
+ grok_loop_kit.egg-info/SOURCES.txt
9
+ grok_loop_kit.egg-info/dependency_links.txt
10
+ grok_loop_kit.egg-info/requires.txt
11
+ grok_loop_kit.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+
2
+ [requests]
3
+ requests>=2.28
@@ -0,0 +1 @@
1
+ grok_loop_kit
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "grok-loop-kit"
7
+ version = "1.0.0"
8
+ description = "Auto-apply Grok 4.5 context compaction to xAI Responses API agent loops."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ keywords = ["grok", "grok-4.5", "xai", "context-compaction", "agent", "responses-api", "llm"]
13
+ dependencies = []
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/Booyaka101/grok-loop-kit"
17
+ Repository = "https://github.com/Booyaka101/grok-loop-kit"
18
+ Issues = "https://github.com/Booyaka101/grok-loop-kit/issues"
19
+
20
+ [project.optional-dependencies]
21
+ # Faster HTTP if you prefer; the package works on stdlib urllib without it.
22
+ requests = ["requests>=2.28"]
23
+
24
+ [tool.setuptools]
25
+ packages = ["grok_loop_kit"]
26
+
27
+ [tool.setuptools.package-data]
28
+ grok_loop_kit = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+