Files
aiturk-hermes-ide/apps/desktop/assets/codex-bridge/gecis_context.py
T
yilsem e7d3080e0a
Server CI and Windows package / verify (push) Failing after 1m40s
Server CI and Windows package / windows-package (push) Skipped
fix: preserve native model switches and bound project searches
2026-10-13 16:32:50 +03:00

259 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Provider-neutral history preparation. Never edits the saved Codex transcript.
Inference routing and voice are not responsibilities of this module. A caller
supplies the target window and an AITURK summarizer; subscription-only requests
must not enter the budget preparation path.
"""
import copy
import hashlib
import json
import re
import threading
from collections import OrderedDict
import ozet_kopru
import compression_settings
PREFIXES = {"custom_tool_call": "ctc", "custom_tool_call_output": "ctco",
"function_call": "fc", "function_call_output": "fco"}
RATIO = 0.70
class TransferError(ValueError):
pass
def encoded(value):
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def tokens(value):
# Deliberately more conservative than the old 3.2 bytes/token estimate.
return (len(encoded(value).encode("utf-8")) + 1) // 2
def input_budget(req, window, settings=None, automatic=False):
limit = int(window * RATIO)
if settings is not None:
limit = (int(window * settings['targetPercent'] / 100) if automatic else
settings['maxTokens'] if settings['budgetMode'] == 'tokens' else
int(window * settings['transferPercent'] / 100))
return min(limit, window - max(1024, int(req.get("max_output_tokens") or 0)))
def foreign(req):
for item in req.get("input") or []:
if not isinstance(item, dict):
continue
identity = str(item.get("id") or "")
want = PREFIXES.get(item.get("type"))
if ("chatcmpl-" in identity or "astrabridge" in identity
or str(item.get("encrypted_content") or "").startswith(ozet_kopru.ISARET)
or (want and identity and not identity.startswith(want + "_"))):
return True
return False
def native_opaque(req):
"""Native encrypted state belongs to the subscription, even in mixed history."""
return any(isinstance(item, dict) and item.get("type") == "compaction" and item.get("encrypted_content")
and not str(item["encrypted_content"]).startswith(ozet_kopru.ISARET)
for item in req.get("input") or [])
def foreign_history(req):
"""Semantic migration evidence, excluding tool ID format mismatches."""
return any(isinstance(item, dict) and (
"chatcmpl-" in str(item.get("id") or "")
or "astrabridge" in str(item.get("id") or "")
or str(item.get("encrypted_content") or "").startswith(ozet_kopru.ISARET))
for item in req.get("input") or [])
def normalize(req):
"""Repair local wrappers/IDs, retaining real OpenAI encrypted compaction.
Only foreign server references (not inline tool results) are removed. Their
visible summary/text, when supplied, is retained as ordinary context.
"""
out = copy.deepcopy(req)
items = out.get("input")
if not isinstance(items, list):
return out
result = []
for item in items:
if not isinstance(item, dict):
result.append(item)
continue
encrypted = item.get("encrypted_content")
if item.get("type") == "compaction" and isinstance(encrypted, str) and encrypted.startswith(ozet_kopru.ISARET):
text = ozet_kopru.ozet_metni(item)
if not text:
raise TransferError("Eski AITURK özeti okunamadı; geçmiş değiştirilmedi.")
result.append(ozet_kopru._ozet_mesaji(text))
continue
identity = item.get("id")
if "chatcmpl-" in str(identity) and item.get("type") in ("reasoning", "item_reference"):
visible = [p.get("text", "") for p in item.get("summary", []) if isinstance(p, dict)]
if any(visible):
result.append(ozet_kopru._ozet_mesaji("\n".join(visible)))
continue
if ("chatcmpl-" in str(identity) or "astrabridge" in str(identity)) and (
item.get("type") == "message" or item.get("role")):
# Inline message content is portable; its foreign server ID is not.
item.pop("id", None)
prefix = PREFIXES.get(item.get("type"))
if prefix and isinstance(identity, str) and identity and not identity.startswith(prefix + "_"):
item["id"] = prefix + "_" + identity.split("_", 1)[-1]
result.append(item)
out["input"] = result
return out
def portable_reasoning(req):
"""Project reasoning onto its visible text for a cross-provider request.
Encrypted reasoning is provider execution state, not a conversation
compaction. Never ask another model to interpret that ciphertext. Messages,
tool pairs and actual compaction items remain intact; callers must still
render opaque compaction with its owner. Native-only requests bypass this
projection, and the saved transcript is never modified.
"""
out = copy.deepcopy(req)
items = out.get("input")
if not isinstance(items, list):
return out
result = []
for item in items:
if isinstance(item, dict) and item.get("type") == "agent_message":
# Peer messages carry delegated tasks/results. External Responses
# translators do not understand this Codex-specific item type.
# Keep provenance as data, never as system/developer authority.
content = item.get("content") or []
if isinstance(content, str):
content = [{"type": "input_text", "text": content}]
portable = []
for part in content:
if isinstance(part, dict) and part.get("type") == "encrypted_content":
# Codex peer delivery can put the literal task/result in
# this wrapper. It is distinct from encrypted reasoning.
# External translators otherwise silently drop the task.
text = part.get("encrypted_content")
if (not isinstance(text, str) or text.startswith("gAAAA")
or re.fullmatch(r"[A-Za-z0-9_+/=-]{128,}", text)):
raise TransferError("Alt ajan iletisi şifreli veya okunamıyor; görev eksik gönderilmedi.")
portable.append({"type": "input_text", "text": text})
else:
portable.append(part)
result.append({"type": "message", "role": "user", "content": [
{"type": "input_text", "text": "Agent message: " + encoded({
"author": item.get("author"), "recipient": item.get("recipient")})}
] + portable})
continue
if not isinstance(item, dict) or item.get("type") != "reasoning":
result.append(item)
continue
visible = []
for field in ("summary", "content"):
for part in item.get(field) or []:
if isinstance(part, dict) and isinstance(part.get("text"), str) and part["text"]:
visible.append(part["text"])
if visible:
# Keep this on the assistant side: a synthetic user message would
# change split_history's count of protected user turns.
result.append({"type": "message", "role": "assistant",
"content": [{"type": "output_text", "text": "\n".join(visible)}]})
out["input"] = result
return out
def split_history(items, keep_users=2):
"""Keep the last two user turns and complete tool pairs; never split a call.
System/developer items are kept verbatim regardless of their position.
"""
users = [i for i, x in enumerate(items) if isinstance(x, dict) and x.get("role") == "user"]
cut = users[-keep_users] if len(users) >= keep_users else (users[0] if users else len(items))
# Some clients put a user/hook message between a call and its result.
while cut:
tail_calls = {x.get("call_id") for x in items[cut:] if isinstance(x, dict) and x.get("call_id")}
earlier = [i for i, x in enumerate(items[:cut]) if isinstance(x, dict) and x.get("call_id") in tail_calls]
if not earlier:
break
cut = min(earlier)
protected, old, recent = [], [], []
for i, item in enumerate(items):
if isinstance(item, dict) and item.get("role") in ("system", "developer"):
protected.append(item)
elif i < cut:
old.append(item)
else:
recent.append(item)
return protected, old, recent
class Preparer:
def __init__(self):
self.cache = OrderedDict()
self.lock = threading.Lock()
def prepare(self, request, window, summarize, scope="", force=False, settings=None,
automatic=False, checkpoints=None):
"""Summarize old history to fit 70%, leaving the original untouched.
summarize(text, max_tokens) must return a completed semantic summary or
raise. Tool execution is forbidden for that separate model request.
Cache keys include the caller scope, exact history and available budget.
"""
req = portable_reasoning(normalize(request))
window = int(window)
if window < 1024:
raise TransferError("Hedef modelin bağlam kapasitesi doğrulanamadı.")
limit = input_budget(req, window, settings, automatic)
keep = settings['keepTurns'] if settings else 2
protected, old, recent = split_history(req.get("input") or [], keep)
original_old = old
saved = checkpoints.get(scope, old, settings) if checkpoints and old else None
if saved:
old = [ozet_kopru._ozet_mesaji(saved[1])] + old[saved[0]:]
req = dict(req, input=protected + old + recent)
if not force and tokens(req) <= limit:
return req
base = dict(req, input=protected + recent)
available = limit - tokens(base) - 256
# Preserve the last user turn even if keeping two turns is too expensive.
if settings is None and (available < 1024 or not old):
protected, old, recent = split_history(req.get("input") or [], keep_users=1)
base = dict(req, input=protected + recent)
available = limit - tokens(base) - 256
if available < 512 or not old:
raise TransferError("Son mesaj ve zorunlu talimatlar hedefe sığmıyor; geçmiş korundu, modele eksik içerik gönderilmedi.")
# Actual compaction/unknown opaque items still require owner rendering.
# Reasoning execution state has already been projected to visible text.
if any(isinstance(x, dict) and x.get("encrypted_content") for x in old):
raise TransferError("Şifreli geçmiş önce sahibi tarafından okunabilir özete çevrilmeli.")
source = encoded(old)
factor = compression_settings.LEVELS[settings['level']] if settings else .5
budget = max(512, int(available * factor))
key = hashlib.sha256((scope + str(budget) + encoded(settings) + source).encode()).hexdigest()
with self.lock:
summary = self.cache.get(key)
if summary is None:
summary = summarize(source, budget)
if not isinstance(summary, str) or len(summary.strip()) < 80:
raise TransferError("AITURK özeti tamamlanamadı; eski geçmiş korundu.")
candidate = dict(req, input=protected + [ozet_kopru._ozet_mesaji(summary)] + recent)
if tokens(candidate) > limit:
raise TransferError("Hazırlanan özet hedef bütçesine sığmadı; eski geçmiş korundu.")
if checkpoints:
checkpoints.save(scope, original_old, settings, summary)
with self.lock:
self.cache[key] = summary
self.cache.move_to_end(key)
while len(self.cache) > 32:
self.cache.popitem(last=False)
return candidate
PREPARER = Preparer()