code-context-control 2.28.0__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.
- cli/__init__.py +1 -0
- cli/_hook_utils.py +99 -0
- cli/c3.py +6152 -0
- cli/commands/__init__.py +1 -0
- cli/commands/common.py +312 -0
- cli/commands/parser.py +286 -0
- cli/docs.html +3178 -0
- cli/edits.html +878 -0
- cli/hook_auto_snapshot.py +142 -0
- cli/hook_c3_signal.py +61 -0
- cli/hook_c3read.py +116 -0
- cli/hook_edit_ledger.py +213 -0
- cli/hook_edit_unlock.py +170 -0
- cli/hook_filter.py +130 -0
- cli/hook_ghost_files.py +238 -0
- cli/hook_pretool_enforce.py +334 -0
- cli/hook_read.py +200 -0
- cli/hook_session_stats.py +62 -0
- cli/hook_terse_advisor.py +190 -0
- cli/hub.html +3764 -0
- cli/hub_server.py +1619 -0
- cli/mcp_proxy.py +428 -0
- cli/mcp_server.py +660 -0
- cli/server.py +2985 -0
- cli/tools/__init__.py +4 -0
- cli/tools/_helpers.py +65 -0
- cli/tools/agent.py +1165 -0
- cli/tools/compress.py +215 -0
- cli/tools/delegate.py +1184 -0
- cli/tools/edit.py +313 -0
- cli/tools/edits.py +118 -0
- cli/tools/filter.py +285 -0
- cli/tools/impact.py +163 -0
- cli/tools/memory.py +469 -0
- cli/tools/read.py +224 -0
- cli/tools/search.py +337 -0
- cli/tools/session.py +95 -0
- cli/tools/shell.py +193 -0
- cli/tools/status.py +306 -0
- cli/tools/validate.py +310 -0
- cli/ui/api.js +36 -0
- cli/ui/app.js +207 -0
- cli/ui/components/chat.js +758 -0
- cli/ui/components/dashboard.js +689 -0
- cli/ui/components/edits.js +220 -0
- cli/ui/components/instructions.js +481 -0
- cli/ui/components/memory.js +626 -0
- cli/ui/components/sessions.js +606 -0
- cli/ui/components/settings.js +1404 -0
- cli/ui/components/sidebar.js +156 -0
- cli/ui/icons.js +51 -0
- cli/ui/shared.js +119 -0
- cli/ui/theme.js +22 -0
- cli/ui.html +168 -0
- cli/ui_legacy.html +6797 -0
- cli/ui_nano.html +503 -0
- code_context_control-2.28.0.dist-info/METADATA +248 -0
- code_context_control-2.28.0.dist-info/RECORD +150 -0
- code_context_control-2.28.0.dist-info/WHEEL +5 -0
- code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
- code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
- code_context_control-2.28.0.dist-info/top_level.txt +5 -0
- core/__init__.py +75 -0
- core/config.py +269 -0
- core/ide.py +188 -0
- oracle/__init__.py +1 -0
- oracle/config.py +75 -0
- oracle/oracle.html +3900 -0
- oracle/oracle_server.py +663 -0
- oracle/services/__init__.py +1 -0
- oracle/services/c3_bridge.py +210 -0
- oracle/services/chat_engine.py +1103 -0
- oracle/services/chat_store.py +155 -0
- oracle/services/cross_memory.py +154 -0
- oracle/services/federated_graph.py +463 -0
- oracle/services/health_checker.py +117 -0
- oracle/services/insight_engine.py +307 -0
- oracle/services/memory_reader.py +106 -0
- oracle/services/memory_writer.py +182 -0
- oracle/services/ollama_bridge.py +332 -0
- oracle/services/project_scanner.py +87 -0
- oracle/services/review_agent.py +206 -0
- services/__init__.py +1 -0
- services/activity_log.py +93 -0
- services/agent_base.py +124 -0
- services/agents.py +1529 -0
- services/auto_memory.py +407 -0
- services/bench/__init__.py +6 -0
- services/bench/external/__init__.py +29 -0
- services/bench/external/aider_polyglot.py +405 -0
- services/bench/external/swe_bench.py +485 -0
- services/benchmark_dashboard.py +596 -0
- services/claude_md.py +785 -0
- services/compressor.py +592 -0
- services/context_snapshot.py +356 -0
- services/conversation_store.py +870 -0
- services/doc_index.py +537 -0
- services/e2e_benchmark.py +2884 -0
- services/e2e_evaluator.py +396 -0
- services/e2e_tasks.py +743 -0
- services/edit_ledger.py +459 -0
- services/embedding_index.py +341 -0
- services/error_reporting.py +123 -0
- services/file_memory.py +734 -0
- services/hub_service.py +585 -0
- services/indexer.py +712 -0
- services/memory.py +318 -0
- services/memory_consolidator.py +538 -0
- services/memory_graph.py +382 -0
- services/memory_grounder.py +304 -0
- services/memory_scorer.py +246 -0
- services/metrics.py +86 -0
- services/notifications.py +209 -0
- services/ollama_client.py +201 -0
- services/output_filter.py +488 -0
- services/parser.py +1238 -0
- services/project_manager.py +579 -0
- services/protocol.py +306 -0
- services/proxy_state.py +152 -0
- services/retrieval_broker.py +129 -0
- services/router.py +414 -0
- services/runtime.py +326 -0
- services/session_benchmark.py +1945 -0
- services/session_manager.py +1026 -0
- services/session_preloader.py +251 -0
- services/text_index.py +90 -0
- services/tool_classifier.py +176 -0
- services/transcript_index.py +340 -0
- services/validation_cache.py +155 -0
- services/vector_store.py +299 -0
- services/version_tracker.py +271 -0
- services/watcher.py +192 -0
- tui/__init__.py +0 -0
- tui/backend.py +59 -0
- tui/main.py +145 -0
- tui/screens/__init__.py +1 -0
- tui/screens/benchmark_view.py +109 -0
- tui/screens/claudemd_view.py +46 -0
- tui/screens/compress_view.py +52 -0
- tui/screens/index_view.py +74 -0
- tui/screens/init_view.py +82 -0
- tui/screens/mcp_view.py +73 -0
- tui/screens/optimize_view.py +41 -0
- tui/screens/pipe_view.py +46 -0
- tui/screens/projects_view.py +355 -0
- tui/screens/search_view.py +55 -0
- tui/screens/session_view.py +143 -0
- tui/screens/stats.py +158 -0
- tui/screens/ui_view.py +54 -0
- tui/theme.tcss +335 -0
services/auto_memory.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""Auto-memory: rule-based learning from tool calls and session activity.
|
|
2
|
+
|
|
3
|
+
Runs in the background after every tool call. Extracts high-signal facts
|
|
4
|
+
from tool results, deduplicates against existing memory, and consolidates
|
|
5
|
+
stale/duplicate facts on session end. No LLM calls — pure rule-based.
|
|
6
|
+
|
|
7
|
+
Wired into mcp_server._finalize_response() and lifespan shutdown.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import threading
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from typing import Any, Dict, List, Tuple
|
|
14
|
+
|
|
15
|
+
_PRIVATE_RE = re.compile(r"<private>.*?</private>", re.DOTALL | re.IGNORECASE)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _strip_private(text: str) -> str:
|
|
19
|
+
"""Remove <private>...</private> blocks before storing or queuing."""
|
|
20
|
+
return _PRIVATE_RE.sub("", text).strip()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AutoMemory:
|
|
24
|
+
"""Automatically extracts and consolidates facts from C3 tool activity."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, memory_store, session_mgr, config: dict | None = None):
|
|
27
|
+
self.memory = memory_store
|
|
28
|
+
self.session_mgr = session_mgr
|
|
29
|
+
self._config = config or {}
|
|
30
|
+
self._queue: list = []
|
|
31
|
+
self._lock = threading.Lock()
|
|
32
|
+
self._worker_running = False
|
|
33
|
+
# Dedup cache: avoid re-extracting the same fact within a session.
|
|
34
|
+
self._recent: set = set()
|
|
35
|
+
self._max_recent = 200
|
|
36
|
+
|
|
37
|
+
# ── Public API ──────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
def on_tool_complete(
|
|
40
|
+
self, tool_name: str, args: dict, summary: str, result_text: str
|
|
41
|
+
):
|
|
42
|
+
"""Queue extraction after a tool call (non-blocking)."""
|
|
43
|
+
if not self._config.get("enabled", True):
|
|
44
|
+
return
|
|
45
|
+
# Only process tools that have extraction rules.
|
|
46
|
+
if tool_name not in _EXTRACTORS:
|
|
47
|
+
return
|
|
48
|
+
with self._lock:
|
|
49
|
+
self._queue.append((tool_name, args, summary, _strip_private(result_text)[:8000]))
|
|
50
|
+
self._ensure_worker()
|
|
51
|
+
|
|
52
|
+
def on_session_end(self):
|
|
53
|
+
"""Called synchronously on session save / snapshot / shutdown."""
|
|
54
|
+
if not self._config.get("enabled", True):
|
|
55
|
+
return
|
|
56
|
+
self._drain_queue()
|
|
57
|
+
self._generate_session_summary()
|
|
58
|
+
|
|
59
|
+
def consolidate(self) -> dict:
|
|
60
|
+
"""Merge duplicate facts and archive stale auto-facts. Returns stats."""
|
|
61
|
+
facts = getattr(self.memory, "facts", None) or []
|
|
62
|
+
if not facts:
|
|
63
|
+
return {"merged": 0, "archived": 0, "total": 0}
|
|
64
|
+
|
|
65
|
+
merged = 0
|
|
66
|
+
archived = 0
|
|
67
|
+
to_delete: set = set()
|
|
68
|
+
|
|
69
|
+
active = [f for f in facts if f.get("lifecycle", "active") == "active"]
|
|
70
|
+
|
|
71
|
+
# ── Merge duplicates (Jaccard > 0.55) ──
|
|
72
|
+
for i, a in enumerate(active):
|
|
73
|
+
if a["id"] in to_delete:
|
|
74
|
+
continue
|
|
75
|
+
for b in active[i + 1 :]:
|
|
76
|
+
if b["id"] in to_delete:
|
|
77
|
+
continue
|
|
78
|
+
sim = _jaccard(a["fact"], b["fact"])
|
|
79
|
+
if sim > 0.55:
|
|
80
|
+
keeper, victim = (
|
|
81
|
+
(a, b)
|
|
82
|
+
if a.get("relevance_count", 0) >= b.get("relevance_count", 0)
|
|
83
|
+
else (b, a)
|
|
84
|
+
)
|
|
85
|
+
if sim < 0.85:
|
|
86
|
+
merged_text = _merge_texts(keeper["fact"], victim["fact"])
|
|
87
|
+
try:
|
|
88
|
+
self.memory.update_fact(
|
|
89
|
+
keeper["id"],
|
|
90
|
+
merged_text,
|
|
91
|
+
keeper.get("category", "general"),
|
|
92
|
+
)
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
to_delete.add(victim["id"])
|
|
96
|
+
merged += 1
|
|
97
|
+
|
|
98
|
+
# ── Archive stale auto-facts (unused for ≥ 7 days) ──
|
|
99
|
+
now = datetime.now(timezone.utc)
|
|
100
|
+
for f in active:
|
|
101
|
+
if f["id"] in to_delete:
|
|
102
|
+
continue
|
|
103
|
+
cat = f.get("category", "")
|
|
104
|
+
if not cat.startswith("auto:"):
|
|
105
|
+
continue
|
|
106
|
+
if f.get("relevance_count", 0) > 0:
|
|
107
|
+
continue
|
|
108
|
+
try:
|
|
109
|
+
age = (now - datetime.fromisoformat(f.get("timestamp", ""))).days
|
|
110
|
+
except (ValueError, TypeError):
|
|
111
|
+
age = 0
|
|
112
|
+
if age >= 7:
|
|
113
|
+
to_delete.add(f["id"])
|
|
114
|
+
archived += 1
|
|
115
|
+
|
|
116
|
+
# ── Rolling window: keep only last _MAX_SESSION_FACTS auto:session entries ──
|
|
117
|
+
_MAX_SESSION_FACTS = 5
|
|
118
|
+
session_facts = sorted(
|
|
119
|
+
[f for f in active if f.get("category") == "auto:session" and f["id"] not in to_delete],
|
|
120
|
+
key=lambda f: f.get("timestamp", ""),
|
|
121
|
+
reverse=True,
|
|
122
|
+
)
|
|
123
|
+
for f in session_facts[_MAX_SESSION_FACTS:]:
|
|
124
|
+
to_delete.add(f["id"])
|
|
125
|
+
archived += 1
|
|
126
|
+
|
|
127
|
+
# ── Archive verbose orphans: >600 chars, 0 recall, 14+ days old ──
|
|
128
|
+
for f in active:
|
|
129
|
+
if f["id"] in to_delete:
|
|
130
|
+
continue
|
|
131
|
+
if len(f.get("fact", "")) <= 600:
|
|
132
|
+
continue
|
|
133
|
+
if f.get("relevance_count", 0) > 0:
|
|
134
|
+
continue
|
|
135
|
+
try:
|
|
136
|
+
age = (now - datetime.fromisoformat(f.get("timestamp", ""))).days
|
|
137
|
+
except (ValueError, TypeError):
|
|
138
|
+
age = 0
|
|
139
|
+
if age >= 14:
|
|
140
|
+
to_delete.add(f["id"])
|
|
141
|
+
archived += 1
|
|
142
|
+
|
|
143
|
+
for fid in to_delete:
|
|
144
|
+
try:
|
|
145
|
+
self.memory.delete_fact(fid)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
"merged": merged,
|
|
151
|
+
"archived": archived,
|
|
152
|
+
"total": len(facts) - len(to_delete),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# ── Background worker ───────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
def _ensure_worker(self):
|
|
158
|
+
if self._worker_running:
|
|
159
|
+
return
|
|
160
|
+
self._worker_running = True
|
|
161
|
+
t = threading.Thread(target=self._worker, daemon=True, name="c3-auto-memory")
|
|
162
|
+
t.start()
|
|
163
|
+
|
|
164
|
+
def _worker(self):
|
|
165
|
+
try:
|
|
166
|
+
self._drain_queue()
|
|
167
|
+
finally:
|
|
168
|
+
self._worker_running = False
|
|
169
|
+
|
|
170
|
+
def _drain_queue(self):
|
|
171
|
+
while True:
|
|
172
|
+
with self._lock:
|
|
173
|
+
if not self._queue:
|
|
174
|
+
return
|
|
175
|
+
item = self._queue.pop(0)
|
|
176
|
+
try:
|
|
177
|
+
self._process(*item)
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
# ── Extraction ──────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
def _process(
|
|
184
|
+
self, tool_name: str, args: dict, summary: str, result_text: str
|
|
185
|
+
):
|
|
186
|
+
extractor = _EXTRACTORS.get(tool_name)
|
|
187
|
+
if not extractor:
|
|
188
|
+
return
|
|
189
|
+
for fact_text, category in extractor(args, summary, result_text):
|
|
190
|
+
self._save_or_merge(fact_text, category)
|
|
191
|
+
|
|
192
|
+
def _save_or_merge(self, fact_text: str, category: str):
|
|
193
|
+
"""Save a new fact or merge with the most similar existing one."""
|
|
194
|
+
fact_text = _strip_private(fact_text)
|
|
195
|
+
if len(fact_text) < 25:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# Session-level dedup.
|
|
199
|
+
key = fact_text[:120].lower()
|
|
200
|
+
if key in self._recent:
|
|
201
|
+
return
|
|
202
|
+
self._recent.add(key)
|
|
203
|
+
if len(self._recent) > self._max_recent:
|
|
204
|
+
self._recent.clear()
|
|
205
|
+
|
|
206
|
+
session_id = ""
|
|
207
|
+
if self.session_mgr and self.session_mgr.current_session:
|
|
208
|
+
session_id = self.session_mgr.current_session.get("id", "")
|
|
209
|
+
|
|
210
|
+
# Check existing facts for a merge candidate.
|
|
211
|
+
try:
|
|
212
|
+
existing = self.memory.recall(fact_text, top_k=3)
|
|
213
|
+
except Exception:
|
|
214
|
+
existing = []
|
|
215
|
+
|
|
216
|
+
for r in existing:
|
|
217
|
+
sim = _jaccard(r.get("fact", ""), fact_text)
|
|
218
|
+
if sim > 0.55:
|
|
219
|
+
if sim < 0.85:
|
|
220
|
+
merged = _merge_texts(r["fact"], fact_text)
|
|
221
|
+
try:
|
|
222
|
+
self.memory.update_fact(
|
|
223
|
+
r["id"], merged, category or r.get("category", "general")
|
|
224
|
+
)
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
return # Already covered by existing fact.
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
self.memory.remember(fact_text, category, session_id)
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
# ── Session summary ─────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
def _generate_session_summary(self):
|
|
237
|
+
"""Build a compact session summary from decisions + file changes."""
|
|
238
|
+
if not self.session_mgr or not self.session_mgr.current_session:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
session = self.session_mgr.current_session
|
|
242
|
+
decisions = session.get("decisions", [])
|
|
243
|
+
files = session.get("files_touched", [])
|
|
244
|
+
|
|
245
|
+
if not decisions and not files:
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
parts: list[str] = []
|
|
249
|
+
|
|
250
|
+
if files:
|
|
251
|
+
names = list(
|
|
252
|
+
dict.fromkeys(f.get("file", "") for f in files if f.get("file"))
|
|
253
|
+
)[:10]
|
|
254
|
+
types = sorted(set(f.get("type", "") for f in files if f.get("type")))
|
|
255
|
+
parts.append(f"Files ({', '.join(types)}): {', '.join(names)}")
|
|
256
|
+
|
|
257
|
+
if decisions:
|
|
258
|
+
for d in decisions[-3:]:
|
|
259
|
+
text = d.get("decision", "")
|
|
260
|
+
if text:
|
|
261
|
+
parts.append(f"Decision: {text}")
|
|
262
|
+
|
|
263
|
+
if not parts:
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
sid = session.get("id", "unknown")[:8]
|
|
267
|
+
summary = f"Session summary ({sid}): " + " | ".join(parts)
|
|
268
|
+
self._save_or_merge(summary, "auto:session")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ── Extraction functions (pure, no side effects) ───────────────────
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _extract_validate(
|
|
275
|
+
args: dict, summary: str, result: str
|
|
276
|
+
) -> List[Tuple[str, str]]:
|
|
277
|
+
"""Extract validation failure patterns."""
|
|
278
|
+
learnings: list = []
|
|
279
|
+
fp = args.get("file_path", "")
|
|
280
|
+
if "FAIL" in result or "syntax_error" in summary:
|
|
281
|
+
error_lines = [l.strip() for l in result.splitlines() if l.strip().startswith("- L")][:3]
|
|
282
|
+
if error_lines:
|
|
283
|
+
learnings.append((
|
|
284
|
+
f"[validate] {fp} has syntax errors: {'; '.join(error_lines)}",
|
|
285
|
+
"auto:validate",
|
|
286
|
+
))
|
|
287
|
+
return learnings
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _extract_search(
|
|
291
|
+
args: dict, summary: str, result: str
|
|
292
|
+
) -> List[Tuple[str, str]]:
|
|
293
|
+
"""Extract key file discoveries."""
|
|
294
|
+
learnings: list = []
|
|
295
|
+
query = args.get("query", "")
|
|
296
|
+
action = args.get("action", "code")
|
|
297
|
+
if action == "files" and query and len(result) > 50:
|
|
298
|
+
files = re.findall(
|
|
299
|
+
r"(?:^|\s)([\w/\\.-]+\.(?:py|js|ts|tsx|jsx|r|rs|go|java|rb|php|lua|pl))\b",
|
|
300
|
+
result,
|
|
301
|
+
re.IGNORECASE,
|
|
302
|
+
)
|
|
303
|
+
if files:
|
|
304
|
+
unique = list(dict.fromkeys(files))[:8]
|
|
305
|
+
learnings.append((
|
|
306
|
+
f"[search] Key files for '{query}': {', '.join(unique)}",
|
|
307
|
+
"auto:structure",
|
|
308
|
+
))
|
|
309
|
+
return learnings
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _extract_compress(
|
|
313
|
+
args: dict, summary: str, result: str
|
|
314
|
+
) -> List[Tuple[str, str]]:
|
|
315
|
+
"""Extract top-level symbols from structural maps."""
|
|
316
|
+
learnings: list = []
|
|
317
|
+
fp = args.get("file_path", "")
|
|
318
|
+
mode = args.get("mode", "")
|
|
319
|
+
if fp and mode in ("map", "dense_map") and len(result) > 100:
|
|
320
|
+
symbols = re.findall(
|
|
321
|
+
r"^(?:def |class |function |export |pub fn |func )\s*(\w+)",
|
|
322
|
+
result,
|
|
323
|
+
re.MULTILINE,
|
|
324
|
+
)
|
|
325
|
+
if symbols:
|
|
326
|
+
unique = list(dict.fromkeys(symbols))[:15]
|
|
327
|
+
learnings.append((
|
|
328
|
+
f"[structure] {fp} exports: {', '.join(unique)}",
|
|
329
|
+
"auto:structure",
|
|
330
|
+
))
|
|
331
|
+
return learnings
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _extract_edit(
|
|
335
|
+
args: dict, summary: str, result: str
|
|
336
|
+
) -> List[Tuple[str, str]]:
|
|
337
|
+
"""Extract what was edited and why (from c3_edit summary arg)."""
|
|
338
|
+
learnings: list = []
|
|
339
|
+
fp = args.get("file_path", "")
|
|
340
|
+
edit_summary = (args.get("summary") or "").strip()
|
|
341
|
+
if fp and edit_summary and len(edit_summary) >= 20:
|
|
342
|
+
learnings.append((
|
|
343
|
+
f"[edit] {fp}: {edit_summary}",
|
|
344
|
+
"auto:edit",
|
|
345
|
+
))
|
|
346
|
+
return learnings
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _extract_agent(
|
|
350
|
+
args: dict, summary: str, result: str
|
|
351
|
+
) -> List[Tuple[str, str]]:
|
|
352
|
+
"""Extract workflow outcomes from c3_agent calls."""
|
|
353
|
+
learnings: list = []
|
|
354
|
+
workflow = args.get("workflow", "")
|
|
355
|
+
scope = args.get("scope", "")
|
|
356
|
+
if workflow and len(result) > 80:
|
|
357
|
+
# First meaningful non-header line from result
|
|
358
|
+
lines = [
|
|
359
|
+
line.strip()
|
|
360
|
+
for line in result.splitlines()
|
|
361
|
+
if line.strip() and not line.strip().startswith("[")
|
|
362
|
+
]
|
|
363
|
+
finding = lines[0][:120] if lines else ""
|
|
364
|
+
if finding:
|
|
365
|
+
scope_str = f" (scope: {scope})" if scope else ""
|
|
366
|
+
learnings.append((
|
|
367
|
+
f"[agent:{workflow}]{scope_str}: {finding}",
|
|
368
|
+
"auto:agent",
|
|
369
|
+
))
|
|
370
|
+
return learnings
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
_EXTRACTORS: Dict[str, Any] = {
|
|
374
|
+
"c3_validate": _extract_validate,
|
|
375
|
+
"c3_search": _extract_search,
|
|
376
|
+
"c3_compress": _extract_compress,
|
|
377
|
+
"c3_edit": _extract_edit,
|
|
378
|
+
"c3_agent": _extract_agent,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ── Utility functions ──────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _jaccard(a: str, b: str) -> float:
|
|
386
|
+
"""Word-level Jaccard similarity."""
|
|
387
|
+
sa = set(a.lower().split())
|
|
388
|
+
sb = set(b.lower().split())
|
|
389
|
+
if not sa or not sb:
|
|
390
|
+
return 0.0
|
|
391
|
+
return len(sa & sb) / len(sa | sb)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _merge_texts(existing: str, new: str) -> str:
|
|
395
|
+
"""Merge two fact texts, preferring the more complete one."""
|
|
396
|
+
if len(new) < len(existing) * 0.5:
|
|
397
|
+
return existing
|
|
398
|
+
if len(existing) < len(new) * 0.5:
|
|
399
|
+
return new
|
|
400
|
+
# Check how much genuinely new content there is.
|
|
401
|
+
existing_words = set(existing.lower().split())
|
|
402
|
+
new_unique = [w for w in new.lower().split() if w not in existing_words]
|
|
403
|
+
if len(new_unique) < 3:
|
|
404
|
+
return existing
|
|
405
|
+
if len(existing) + len(new) > 500:
|
|
406
|
+
return new # Newer is more current; avoid bloat.
|
|
407
|
+
return f"{existing} [updated] {new}"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""External benchmark adapters."""
|
|
2
|
+
|
|
3
|
+
from services.bench.external.aider_polyglot import (
|
|
4
|
+
AiderPolyglotBenchmark,
|
|
5
|
+
AiderPolyglotResult,
|
|
6
|
+
detect_aider,
|
|
7
|
+
find_polyglot_repo,
|
|
8
|
+
)
|
|
9
|
+
from services.bench.external.swe_bench import (
|
|
10
|
+
SWEBenchAdapter,
|
|
11
|
+
SWEBenchReport,
|
|
12
|
+
SWEBenchResult,
|
|
13
|
+
SWEBenchTask,
|
|
14
|
+
evaluate_with_docker,
|
|
15
|
+
load_tasks,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AiderPolyglotBenchmark",
|
|
20
|
+
"AiderPolyglotResult",
|
|
21
|
+
"detect_aider",
|
|
22
|
+
"find_polyglot_repo",
|
|
23
|
+
"SWEBenchAdapter",
|
|
24
|
+
"SWEBenchTask",
|
|
25
|
+
"SWEBenchResult",
|
|
26
|
+
"SWEBenchReport",
|
|
27
|
+
"load_tasks",
|
|
28
|
+
"evaluate_with_docker",
|
|
29
|
+
]
|