@mrtrinhvn/ag-kit 1.3.1 → 1.4.0
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.
- package/README.md +32 -46
- package/bin/cli.js +79 -201
- package/package.json +1 -1
- package/template/.agent/knowledge/USER.md +4 -0
- package/template/.agent/knowledge/ag-kit-ecosystem.md +168 -0
- package/template/.agent/knowledge/ag-kit-elite.md +6 -3
- package/template/.agent/knowledge/context_isolation.md +16 -0
- package/template/.agent/scripts/_post-commit-hook +8 -0
- package/template/.agent/scripts/brain_builder.py +287 -0
- package/template/.agent/scripts/memory_mcp_server.py +423 -0
- package/template/.agent/scripts/memory_tool.py +367 -0
- package/template/.agent/scripts/receptionist_down.sh +5 -5
- package/template/.agent/scripts/receptionist_up.sh +13 -10
- package/template/.agent/scripts/refresh_brain.sh +21 -0
- package/template/.agent/scripts/repomap.py +32 -3
- package/template/.agent/skills/ag-kit-core/SKILL.md +88 -2
- package/template/.agent/skills/intelligent-routing/SKILL.md +20 -10
- package/template/.agent/skills/telegram-agentic-gateway/SKILL.md +3 -0
- package/template/.agent/skills/telegram-agentic-gateway/templates/start.sh.template +2 -2
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
memory_tool.py — Ag-Kit Two-Tier Memory System (Option B)
|
|
4
|
+
|
|
5
|
+
TIER 1 (HOT) — Working Memory: Fast write/read, exact + semantic search
|
|
6
|
+
Save immediately: `save "content" --category decision`
|
|
7
|
+
Search: `search "keyword"` (uses both LIKE and Cosine Similarity if model present)
|
|
8
|
+
List hot nodes: `hot`
|
|
9
|
+
|
|
10
|
+
TIER 2 (COLD) — Long-term Memory: Consolidated summaries
|
|
11
|
+
Consolidate hot → cold: `consolidate [--category X] [--days 7]`
|
|
12
|
+
Search cold tier: `cold` | `search "kw" --tier cold`
|
|
13
|
+
|
|
14
|
+
EMBEDDINGS (Option B - Dual-Model Coordination):
|
|
15
|
+
Automatically calls local Ollama `nomic-embed-text` during save & search.
|
|
16
|
+
No external libraries required (pure python struct/math).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import sqlite3
|
|
21
|
+
import json
|
|
22
|
+
import urllib.request
|
|
23
|
+
import struct
|
|
24
|
+
import math
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from datetime import datetime, timedelta
|
|
27
|
+
|
|
28
|
+
DEFAULT_DB = Path(__file__).parent.parent / "memory" / "graph.db"
|
|
29
|
+
VALID_CATEGORIES = {"user_pref", "decision", "error", "pattern", "context", "general"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ─── Option B: Vector Embeddings (Ollama) ─────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
def get_embedding(text: str) -> bytes:
|
|
35
|
+
"""Calls local Ollama nomic-embed-text and returns packed bytes."""
|
|
36
|
+
try:
|
|
37
|
+
req = urllib.request.Request(
|
|
38
|
+
"http://localhost:11434/api/embed",
|
|
39
|
+
json.dumps({"model": "nomic-embed-text", "input": text}).encode('utf-8')
|
|
40
|
+
)
|
|
41
|
+
with urllib.request.urlopen(req, timeout=10) as res:
|
|
42
|
+
data = json.loads(res.read().decode('utf-8'))
|
|
43
|
+
floats = data.get("embeddings", [[]])[0]
|
|
44
|
+
if not floats:
|
|
45
|
+
return None
|
|
46
|
+
return struct.pack(f"{len(floats)}f", *floats)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
# Silently fail if Ollama is not running or model missing
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def cosine_similarity(v1: bytes, v2: bytes) -> float:
|
|
52
|
+
"""Computes cosine similarity between two byte-packed float vectors."""
|
|
53
|
+
if not v1 or not v2: return 0.0
|
|
54
|
+
try:
|
|
55
|
+
f1 = struct.unpack(f"{len(v1)//4}f", v1)
|
|
56
|
+
f2 = struct.unpack(f"{len(v2)//4}f", v2)
|
|
57
|
+
dot = sum(a * b for a, b in zip(f1, f2))
|
|
58
|
+
norm1 = math.sqrt(sum(a * a for a in f1))
|
|
59
|
+
norm2 = math.sqrt(sum(b * b for b in f2))
|
|
60
|
+
if norm1 == 0 or norm2 == 0: return 0.0
|
|
61
|
+
return dot / (norm1 * norm2)
|
|
62
|
+
except Exception:
|
|
63
|
+
return 0.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ─── DB Setup ─────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
def get_db(db_path: Path) -> sqlite3.Connection:
|
|
69
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
conn = sqlite3.connect(str(db_path))
|
|
71
|
+
conn.row_factory = sqlite3.Row
|
|
72
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
73
|
+
_init_schema(conn)
|
|
74
|
+
return conn
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _init_schema(conn: sqlite3.Connection):
|
|
78
|
+
conn.executescript("""
|
|
79
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
80
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
+
content TEXT NOT NULL,
|
|
82
|
+
category TEXT NOT NULL DEFAULT 'general',
|
|
83
|
+
energy INTEGER NOT NULL DEFAULT 100,
|
|
84
|
+
tier TEXT NOT NULL DEFAULT 'hot',
|
|
85
|
+
source_ids TEXT,
|
|
86
|
+
embedding BLOB,
|
|
87
|
+
created_at TEXT NOT NULL,
|
|
88
|
+
updated_at TEXT NOT NULL
|
|
89
|
+
);
|
|
90
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
91
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
|
+
from_node INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
93
|
+
to_node INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
94
|
+
relation TEXT NOT NULL DEFAULT 'related_to',
|
|
95
|
+
created_at TEXT NOT NULL
|
|
96
|
+
);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_category ON nodes(category);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_tier ON nodes(tier);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_energy ON nodes(energy);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_node);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_node);
|
|
102
|
+
""")
|
|
103
|
+
# Live migration
|
|
104
|
+
cols = [r[1] for r in conn.execute("PRAGMA table_info(nodes)").fetchall()]
|
|
105
|
+
for col, typedef in [("tier", "TEXT DEFAULT 'hot'"), ("source_ids", "TEXT"), ("embedding", "BLOB")]:
|
|
106
|
+
if col not in cols:
|
|
107
|
+
conn.execute(f"ALTER TABLE nodes ADD COLUMN {col} {typedef}")
|
|
108
|
+
conn.commit()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ─── HOT TIER ─────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
def cmd_save(args, conn):
|
|
114
|
+
now = datetime.now().isoformat()
|
|
115
|
+
category = args.category if args.category in VALID_CATEGORIES else "general"
|
|
116
|
+
|
|
117
|
+
emb = get_embedding(args.content)
|
|
118
|
+
|
|
119
|
+
cur = conn.execute(
|
|
120
|
+
"INSERT INTO nodes (content, category, energy, tier, embedding, created_at, updated_at) VALUES (?, ?, 100, 'hot', ?, ?, ?)",
|
|
121
|
+
(args.content, category, emb, now, now)
|
|
122
|
+
)
|
|
123
|
+
conn.commit()
|
|
124
|
+
emb_status = "✨[Vector]" if emb else "⚠️[No-Vector]"
|
|
125
|
+
print(f"✅ {emb_status} Saved hot node #{cur.lastrowid} [{category}]: {args.content[:80]}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def cmd_hot(args, conn):
|
|
129
|
+
rows = conn.execute(
|
|
130
|
+
"SELECT id, category, energy, content, embedding, created_at FROM nodes WHERE tier='hot' ORDER BY energy DESC, updated_at DESC LIMIT ?",
|
|
131
|
+
(args.limit,)
|
|
132
|
+
).fetchall()
|
|
133
|
+
print(f"🔥 HOT nodes ({len(rows)} shown):\n")
|
|
134
|
+
for r in rows:
|
|
135
|
+
v_mark = "✨" if r["embedding"] else " "
|
|
136
|
+
print(f" {v_mark}#{r['id']} [{r['category']}] ⚡{r['energy']} 📅{r['created_at'][:10]}")
|
|
137
|
+
print(f" {r['content'][:120]}\n")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ─── CONSOLIDATION (Hot → Cold) ───────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
def cmd_consolidate(args, conn):
|
|
143
|
+
threshold_date = (datetime.now() - timedelta(days=args.days)).isoformat()
|
|
144
|
+
cat_filter = f"AND category = '{args.category}'" if args.category else ""
|
|
145
|
+
|
|
146
|
+
hot_rows = conn.execute(
|
|
147
|
+
f"SELECT id, content, category FROM nodes WHERE tier='hot' AND updated_at < ? {cat_filter} ORDER BY category, energy DESC",
|
|
148
|
+
(threshold_date,)
|
|
149
|
+
).fetchall()
|
|
150
|
+
|
|
151
|
+
if not hot_rows:
|
|
152
|
+
print(f"⚡ No hot nodes older than {args.days} days to consolidate.")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
from collections import defaultdict
|
|
156
|
+
groups = defaultdict(list)
|
|
157
|
+
for r in hot_rows:
|
|
158
|
+
groups[r["category"]].append(r)
|
|
159
|
+
|
|
160
|
+
consolidated_count = 0
|
|
161
|
+
now = datetime.now().isoformat()
|
|
162
|
+
|
|
163
|
+
for category, nodes in groups.items():
|
|
164
|
+
if len(nodes) < 2 and not args.force:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
node_ids = [r["id"] for r in nodes]
|
|
168
|
+
merged = "\n".join(f"• {r['content'][:200]}" for r in nodes[:20])
|
|
169
|
+
summary = f"[Consolidated {len(nodes)} nodes — {category}]\n{merged}"
|
|
170
|
+
|
|
171
|
+
emb = get_embedding(summary)
|
|
172
|
+
|
|
173
|
+
cur = conn.execute(
|
|
174
|
+
"INSERT INTO nodes (content, category, energy, tier, source_ids, embedding, created_at, updated_at) VALUES (?, ?, 80, 'cold', ?, ?, ?, ?)",
|
|
175
|
+
(summary, category, json.dumps(node_ids), emb, now, now)
|
|
176
|
+
)
|
|
177
|
+
cold_id = cur.lastrowid
|
|
178
|
+
|
|
179
|
+
for nid in node_ids:
|
|
180
|
+
conn.execute(
|
|
181
|
+
"INSERT INTO edges (from_node, to_node, relation, created_at) VALUES (?, ?, 'consolidated_into', ?)",
|
|
182
|
+
(nid, cold_id, now)
|
|
183
|
+
)
|
|
184
|
+
conn.execute("UPDATE nodes SET energy = MAX(0, energy - 40) WHERE id = ?", (nid,))
|
|
185
|
+
|
|
186
|
+
consolidated_count += 1
|
|
187
|
+
print(f"❄️ [COLD] '{category}': merged {len(nodes)} hot nodes → #{cold_id}")
|
|
188
|
+
|
|
189
|
+
conn.commit()
|
|
190
|
+
print(f"\n✅ Consolidation complete. Run `gc` to cleanup dead hot nodes.")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ─── COLD TIER ────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
def cmd_cold(args, conn):
|
|
196
|
+
rows = conn.execute(
|
|
197
|
+
"SELECT id, category, energy, content, source_ids, updated_at FROM nodes WHERE tier='cold' ORDER BY energy DESC, updated_at DESC LIMIT ?",
|
|
198
|
+
(args.limit,)
|
|
199
|
+
).fetchall()
|
|
200
|
+
print(f"❄️ COLD nodes ({len(rows)} shown):\n")
|
|
201
|
+
for r in rows:
|
|
202
|
+
sources = json.loads(r["source_ids"] or "[]")
|
|
203
|
+
print(f" #{r['id']} [{r['category']}] ⚡{r['energy']} (from {len(sources)} hot nodes)")
|
|
204
|
+
print(f" {r['content'][:200]}\n")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ─── GRAPH (Edges) ────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
def cmd_link(args, conn):
|
|
210
|
+
now = datetime.now().isoformat()
|
|
211
|
+
a = conn.execute("SELECT id, content FROM nodes WHERE id = ?", (args.from_id,)).fetchone()
|
|
212
|
+
b = conn.execute("SELECT id, content FROM nodes WHERE id = ?", (args.to_id,)).fetchone()
|
|
213
|
+
if not a or not b:
|
|
214
|
+
print(f"❌ Node #{args.from_id} or #{args.to_id} not found.")
|
|
215
|
+
return
|
|
216
|
+
conn.execute(
|
|
217
|
+
"INSERT INTO edges (from_node, to_node, relation, created_at) VALUES (?, ?, ?, ?)",
|
|
218
|
+
(args.from_id, args.to_id, args.relation, now)
|
|
219
|
+
)
|
|
220
|
+
conn.commit()
|
|
221
|
+
print(f"🔗 Linked: #{args.from_id} --[{args.relation}]--> #{args.to_id}")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def cmd_graph(args, conn):
|
|
225
|
+
node = conn.execute("SELECT * FROM nodes WHERE id = ?", (args.node_id,)).fetchone()
|
|
226
|
+
if not node:
|
|
227
|
+
print(f"❌ Node #{args.node_id} not found.")
|
|
228
|
+
return
|
|
229
|
+
print(f"🕸️ Graph around Node #{args.node_id} [{node['tier'].upper()}]:\n")
|
|
230
|
+
print(f" 📌 #{node['id']} [{node['category']}] ⚡{node['energy']} — {node['content'][:150]}\n")
|
|
231
|
+
|
|
232
|
+
outgoing = conn.execute("SELECT e.relation, e.to_node, n.content FROM edges e JOIN nodes n ON e.to_node=n.id WHERE e.from_node=?", (args.node_id,)).fetchall()
|
|
233
|
+
if outgoing:
|
|
234
|
+
print(f" → OUT ({len(outgoing)}):")
|
|
235
|
+
for e in outgoing: print(f" --[{e['relation']}]--> #{e['to_node']}: {e['content'][:80]}")
|
|
236
|
+
|
|
237
|
+
incoming = conn.execute("SELECT e.relation, e.from_node, n.content FROM edges e JOIN nodes n ON e.from_node=n.id WHERE e.to_node=?", (args.node_id,)).fetchall()
|
|
238
|
+
if incoming:
|
|
239
|
+
print(f"\n ← IN ({len(incoming)}):")
|
|
240
|
+
for e in incoming: print(f" #{e['from_node']}: {e['content'][:80]} --[{e['relation']}]-->")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ─── SEARCH ───────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
def cmd_search(args, conn):
|
|
246
|
+
keyword = args.keyword.strip()
|
|
247
|
+
tier_filter = f"AND tier = '{args.tier}'" if args.tier else ""
|
|
248
|
+
|
|
249
|
+
# 1. Exact Match Search
|
|
250
|
+
exact_rows = conn.execute(
|
|
251
|
+
f"SELECT id, tier, category, energy, content, created_at FROM nodes WHERE content LIKE ? {tier_filter} ORDER BY energy DESC LIMIT ?",
|
|
252
|
+
(f"%{keyword}%", args.limit)
|
|
253
|
+
).fetchall()
|
|
254
|
+
|
|
255
|
+
results = []
|
|
256
|
+
seen = set()
|
|
257
|
+
|
|
258
|
+
for r in exact_rows:
|
|
259
|
+
seen.add(r["id"])
|
|
260
|
+
results.append({"row": r, "score": 1.0, "match": "exact"})
|
|
261
|
+
|
|
262
|
+
# 2. Semantic Search (Cosine Similarity)
|
|
263
|
+
query_emb = get_embedding(keyword)
|
|
264
|
+
if query_emb:
|
|
265
|
+
all_nodes = conn.execute(
|
|
266
|
+
f"SELECT id, tier, category, energy, content, created_at, embedding FROM nodes WHERE embedding IS NOT NULL {tier_filter}"
|
|
267
|
+
).fetchall()
|
|
268
|
+
for r in all_nodes:
|
|
269
|
+
if r["id"] in seen: continue
|
|
270
|
+
sim = cosine_similarity(query_emb, r["embedding"])
|
|
271
|
+
if sim > 0.60:
|
|
272
|
+
results.append({"row": dict(r), "score": sim, "match": f"semantic: {sim:.2f}"})
|
|
273
|
+
|
|
274
|
+
# Sort & Limit
|
|
275
|
+
results.sort(key=lambda x: (x["score"], x["row"]["energy"]), reverse=True)
|
|
276
|
+
results = results[:args.limit]
|
|
277
|
+
|
|
278
|
+
tier_label = f" [{args.tier.upper()}]" if args.tier else ""
|
|
279
|
+
if not results:
|
|
280
|
+
print(f"🔍 No results for '{keyword}'{tier_label}")
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
print(f"🔍 Found {len(results)} result(s) for '{keyword}'{tier_label}:\n")
|
|
284
|
+
for res in results:
|
|
285
|
+
r = res["row"]
|
|
286
|
+
tier_icon = "🔥" if r["tier"] == "hot" else "❄️"
|
|
287
|
+
print(f" {tier_icon} #{r['id']} [{r['category']}] ⚡{r['energy']} ({res['match']}) — {r['content'][:120]}")
|
|
288
|
+
|
|
289
|
+
ids = [res["row"]["id"] for res in results]
|
|
290
|
+
if ids:
|
|
291
|
+
conn.execute(
|
|
292
|
+
f"UPDATE nodes SET energy = MIN(100, energy + 10), updated_at = ? WHERE id IN ({','.join('?'*len(ids))})",
|
|
293
|
+
[datetime.now().isoformat()] + ids
|
|
294
|
+
)
|
|
295
|
+
conn.commit()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ─── GC & LIST (Truncated Logic) ──────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
def cmd_gc(args, conn):
|
|
301
|
+
threshold = (datetime.now() - timedelta(days=args.days)).isoformat()
|
|
302
|
+
conn.execute("UPDATE nodes SET energy = energy - 15 WHERE updated_at < ? AND tier = 'hot'", (threshold,))
|
|
303
|
+
cur = conn.execute("DELETE FROM nodes WHERE energy <= 0")
|
|
304
|
+
conn.commit()
|
|
305
|
+
print(f"🗑️ GC: {cur.rowcount} dead nodes removed.")
|
|
306
|
+
|
|
307
|
+
def cmd_list(args, conn):
|
|
308
|
+
rows = conn.execute("SELECT id, tier, category, energy, content FROM nodes ORDER BY energy DESC LIMIT ?", (args.limit,)).fetchall()
|
|
309
|
+
hot_c = sum(1 for r in rows if r["tier"] == "hot")
|
|
310
|
+
print(f"📋 {hot_c} hot 🔥 | {len(rows)-hot_c} cold ❄️")
|
|
311
|
+
for r in rows:
|
|
312
|
+
print(f" #{r['id']} [{r['category']}] ⚡{r['energy']} — {r['content'][:80]}")
|
|
313
|
+
|
|
314
|
+
def cmd_export(args, conn):
|
|
315
|
+
tier_filter = f"WHERE tier = '{args.tier}'" if args.tier else ""
|
|
316
|
+
nodes = conn.execute(f"SELECT id, content, category, tier, energy, source_ids, created_at FROM nodes {tier_filter}").fetchall()
|
|
317
|
+
edges = conn.execute("SELECT * FROM edges").fetchall()
|
|
318
|
+
f = Path(args.output or ".agent/memory/export.json")
|
|
319
|
+
f.parent.mkdir(parents=True, exist_ok=True)
|
|
320
|
+
f.write_text(json.dumps({"nodes": [dict(r) for r in nodes], "edges": [dict(r) for r in edges]}, indent=2, ensure_ascii=False))
|
|
321
|
+
print(f"✅ Exported {len(nodes)} nodes to {f}")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ─── Main ─────────────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
def main():
|
|
327
|
+
p = argparse.ArgumentParser()
|
|
328
|
+
p.add_argument("--db", default=str(DEFAULT_DB))
|
|
329
|
+
sub = p.add_subparsers(dest="cmd")
|
|
330
|
+
|
|
331
|
+
ps = sub.add_parser("save")
|
|
332
|
+
ps.add_argument("content")
|
|
333
|
+
ps.add_argument("--category", "-c", default="general")
|
|
334
|
+
|
|
335
|
+
pf = sub.add_parser("search")
|
|
336
|
+
pf.add_argument("keyword")
|
|
337
|
+
pf.add_argument("--limit", "-n", type=int, default=10)
|
|
338
|
+
pf.add_argument("--tier", choices=["hot", "cold"], default=None)
|
|
339
|
+
|
|
340
|
+
sub.add_parser("hot").add_argument("--limit", "-n", type=int, default=20)
|
|
341
|
+
sub.add_parser("cold").add_argument("--limit", "-n", type=int, default=20)
|
|
342
|
+
sub.add_parser("list").add_argument("--limit", "-n", type=int, default=20)
|
|
343
|
+
|
|
344
|
+
pl = sub.add_parser("link")
|
|
345
|
+
pl.add_argument("from_id", type=int); pl.add_argument("to_id", type=int); pl.add_argument("--relation", "-r", default="related_to")
|
|
346
|
+
|
|
347
|
+
sub.add_parser("graph").add_argument("node_id", type=int)
|
|
348
|
+
|
|
349
|
+
pcon = sub.add_parser("consolidate")
|
|
350
|
+
pcon.add_argument("--days", type=int, default=7); pcon.add_argument("--category", default=None); pcon.add_argument("--force", action="store_true")
|
|
351
|
+
|
|
352
|
+
sub.add_parser("gc").add_argument("--days", type=int, default=30)
|
|
353
|
+
|
|
354
|
+
pex = sub.add_parser("export")
|
|
355
|
+
pex.add_argument("--output", "-o", default=None); pex.add_argument("--tier", default=None)
|
|
356
|
+
|
|
357
|
+
args = p.parse_args()
|
|
358
|
+
if not args.cmd: return p.print_help()
|
|
359
|
+
|
|
360
|
+
conn = get_db(Path(args.db))
|
|
361
|
+
try:
|
|
362
|
+
globals()["cmd_"+args.cmd](args, conn)
|
|
363
|
+
finally:
|
|
364
|
+
conn.close()
|
|
365
|
+
|
|
366
|
+
if __name__ == "__main__":
|
|
367
|
+
main()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
3
|
# --- Antigravity Unified Shutdown (Golden V6 - High Precision) ---
|
|
4
|
-
# Dọn dẹp trạm gác
|
|
4
|
+
# Dọn dẹp trạm gác Agentic Gateway.
|
|
5
5
|
|
|
6
6
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
7
7
|
cd "$PROJECT_DIR"
|
|
@@ -13,7 +13,7 @@ fi
|
|
|
13
13
|
BRIDGE_PORT=${BRIDGE_PORT:-9558}
|
|
14
14
|
|
|
15
15
|
echo "════════════════════════════════════════════════════"
|
|
16
|
-
echo " 🛑 SHUTTING DOWN
|
|
16
|
+
echo " 🛑 SHUTTING DOWN AG GATEWAY [$BRIDGE_PORT]..."
|
|
17
17
|
echo "════════════════════════════════════════════════════"
|
|
18
18
|
|
|
19
19
|
# 1. Tắt Sentinel Monitor
|
|
@@ -31,15 +31,15 @@ if [ -f "$BRIDGE_PID_FILE" ]; then
|
|
|
31
31
|
fi
|
|
32
32
|
|
|
33
33
|
# 3. Tắt Bot
|
|
34
|
-
BOT_PID_FILE=".
|
|
34
|
+
BOT_PID_FILE=".ag_gateway_bot.pid"
|
|
35
35
|
if [ -f "$BOT_PID_FILE" ]; then
|
|
36
36
|
PID=$(cat "$BOT_PID_FILE")
|
|
37
37
|
# Kill the whole process group for the bash loop
|
|
38
38
|
pkill -P $PID
|
|
39
39
|
kill $PID 2>/dev/null && rm "$BOT_PID_FILE"
|
|
40
|
-
echo "✅
|
|
40
|
+
echo "✅ AG Gateway Bot stopped."
|
|
41
41
|
fi
|
|
42
42
|
|
|
43
43
|
echo "════════════════════════════════════════════════════"
|
|
44
|
-
echo "✅
|
|
44
|
+
echo "✅ AG GATEWAY IS IDLE."
|
|
45
45
|
echo "════════════════════════════════════════════════════"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
3
|
# --- Antigravity Unified Receptionist (Golden V6 - High Precision) ---
|
|
4
|
-
# Trạm gác hợp nhất:
|
|
5
|
-
# Dự án:
|
|
4
|
+
# Trạm gác hợp nhất: AG Gateway Bot + Portal Bridge (HUD)
|
|
5
|
+
# Dự án: Template Agentic Orchestrator
|
|
6
6
|
|
|
7
7
|
# 1. Load Cấu hình & Xử lý Cổng Thông minh
|
|
8
8
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
@@ -17,7 +17,7 @@ else
|
|
|
17
17
|
fi
|
|
18
18
|
|
|
19
19
|
echo "════════════════════════════════════════════════════"
|
|
20
|
-
echo " 🏢
|
|
20
|
+
echo " 🏢 AG GATEWAY - PORTAL [$BRIDGE_PORT]"
|
|
21
21
|
echo "════════════════════════════════════════════════════"
|
|
22
22
|
|
|
23
23
|
# 2. Xử lý Chế độ Headless & Cloudflare Tunnel
|
|
@@ -99,13 +99,16 @@ nohup node scripts/ag_portal_bridge.js > ".portal_bridge_${BRIDGE_PORT}.log" 2>&
|
|
|
99
99
|
echo $! > "$BRIDGE_PID_FILE"
|
|
100
100
|
echo " ✅ Bridge running (PID: $(cat "$BRIDGE_PID_FILE") on Port $BRIDGE_PORT)"
|
|
101
101
|
|
|
102
|
-
# 4. Khởi động
|
|
103
|
-
echo "[4/4] 🤖 Waking up
|
|
104
|
-
BOT_PID_FILE=".
|
|
102
|
+
# 4. Khởi động AG Gateway Bot (Trợ lý Telegram)
|
|
103
|
+
echo "[4/4] 🤖 Waking up AG Gateway Bot... "
|
|
104
|
+
BOT_PID_FILE=".ag_gateway_bot.pid"
|
|
105
105
|
|
|
106
106
|
# Kill loop AND all tsx/node processes belonging to this token/project
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
# Chỉ kill nếu dự án có thư mục src/index.ts (tránh kill nhầm project khác chạy chung server)
|
|
108
|
+
if [ -f "src/index.ts" ] || [ -f "bot/index.ts" ]; then
|
|
109
|
+
pkill -f "tsx src/index.ts" 2>/dev/null
|
|
110
|
+
pkill -f "node src/index.ts" 2>/dev/null
|
|
111
|
+
fi
|
|
109
112
|
pkill -f "interaction_final.log" 2>/dev/null
|
|
110
113
|
if [ -f "$BOT_PID_FILE" ]; then
|
|
111
114
|
kill -9 $(cat "$BOT_PID_FILE") 2>/dev/null
|
|
@@ -118,11 +121,11 @@ nohup bash -c "while true; do
|
|
|
118
121
|
npx tsx src/index.ts >> interaction_final.log 2>&1
|
|
119
122
|
EXIT_CODE=\$?
|
|
120
123
|
if [ \$EXIT_CODE -eq 0 ]; then break; fi # Exit loop if deliberate stop
|
|
121
|
-
echo \"\$(date): Bot crashed (Code: \$EXIT_CODE). Restarting in 5s...\" >> interaction_final.log
|
|
124
|
+
echo \"\$(date): AG Gateway Bot crashed (Code: \$EXIT_CODE). Restarting in 5s...\" >> interaction_final.log
|
|
122
125
|
sleep 5
|
|
123
126
|
done" &
|
|
124
127
|
echo $! > "$BOT_PID_FILE"
|
|
125
|
-
echo " ✅ Bot active (PID: $(cat "$BOT_PID_FILE"))"
|
|
128
|
+
echo " ✅ AG Gateway Bot active (PID: $(cat "$BOT_PID_FILE"))"
|
|
126
129
|
|
|
127
130
|
# 5. Sentinel Monitor (Giám sát Trí não)
|
|
128
131
|
echo "🛡️ Sentinel Active: Auto-shutdown if IDE connection lost."
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# refresh_brain.sh — Tầng 1 Memory: Rebuild codebase index for the current project
|
|
3
|
+
# Usage: bash .agent/scripts/refresh_brain.sh [root_dir]
|
|
4
|
+
# Output: .agent/cache/repomap.json
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
ROOT="${1:-.}"
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
OUTPUT="$ROOT/.agent/cache/repomap.json"
|
|
10
|
+
|
|
11
|
+
echo "🧠 Refreshing Codebase Brain (Tầng 1)..."
|
|
12
|
+
echo " Root: $(realpath $ROOT)"
|
|
13
|
+
echo " Output: $OUTPUT"
|
|
14
|
+
|
|
15
|
+
python3 "$SCRIPT_DIR/repomap.py" "$ROOT" --output "$OUTPUT" --max-files 200
|
|
16
|
+
|
|
17
|
+
echo ""
|
|
18
|
+
echo "✅ Codebase index updated at: $OUTPUT"
|
|
19
|
+
echo " AI agents can now read this file instead of re-scanning the entire codebase."
|
|
20
|
+
echo ""
|
|
21
|
+
echo "📌 Next: Run memory_tool.py to access Tầng 2 (Working Memory Graph)"
|
|
@@ -121,6 +121,35 @@ class RepoMapper:
|
|
|
121
121
|
return "\n".join(repo_map)
|
|
122
122
|
|
|
123
123
|
if __name__ == "__main__":
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
import argparse
|
|
125
|
+
import json
|
|
126
|
+
from datetime import datetime
|
|
127
|
+
|
|
128
|
+
parser = argparse.ArgumentParser(description="RepoMapper: Generate a structural map of a codebase.")
|
|
129
|
+
parser.add_argument("root", nargs="?", default=".", help="Root directory to scan (default: current dir)")
|
|
130
|
+
parser.add_argument("--output", "-o", help="Output file path (e.g. .agent/cache/repomap.json). If not set, prints to stdout.")
|
|
131
|
+
parser.add_argument("--max-files", type=int, default=100, help="Max files to scan (default: 100)")
|
|
132
|
+
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format (default: text)")
|
|
133
|
+
args = parser.parse_args()
|
|
134
|
+
|
|
135
|
+
mapper = RepoMapper(args.root)
|
|
136
|
+
text_map = mapper.generate_map(max_files=args.max_files)
|
|
137
|
+
|
|
138
|
+
if args.output:
|
|
139
|
+
output_path = Path(args.output)
|
|
140
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
|
|
142
|
+
if args.format == "json" or args.output.endswith(".json"):
|
|
143
|
+
data = {
|
|
144
|
+
"generated_at": datetime.now().isoformat(),
|
|
145
|
+
"root": str(Path(args.root).absolute()),
|
|
146
|
+
"map": text_map,
|
|
147
|
+
"lines": text_map.splitlines()
|
|
148
|
+
}
|
|
149
|
+
output_path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
|
150
|
+
else:
|
|
151
|
+
output_path.write_text(text_map)
|
|
152
|
+
|
|
153
|
+
print(f"✅ RepoMap saved to: {output_path.absolute()}")
|
|
154
|
+
else:
|
|
155
|
+
print(text_map)
|
|
@@ -12,7 +12,8 @@ allowed-tools: Read, Write, Glob, Grep, Commands
|
|
|
12
12
|
|
|
13
13
|
## 1. 🧬 KHẲNG ĐỊNH ĐỊNH DANH (IDENTITY BOOTSTRAP)
|
|
14
14
|
|
|
15
|
-
- **Cội nguồn:** Bạn ra đời từ khuôn mẫu **`ag-kit` (Antigravity Kit)** - một Framework giúp tự động hóa và quản trị các AI Agents
|
|
15
|
+
- **Cội nguồn:** Bạn ra đời từ khuôn mẫu **`ag-kit` (Antigravity Kit)** - một Framework riêng tư của Owner (một cá nhân duy nhất), giúp tự động hóa và quản trị các AI Agents trên nhiều dự án.
|
|
16
|
+
- **Hệ sinh thái:** Bạn đang ở trong **một trong nhiều dự án** thuộc hệ sinh thái ag-kit. Đọc `.agent/knowledge/ag-kit-ecosystem.md` để biết toàn cảnh.
|
|
16
17
|
- **Vai trò:** Bạn đóng vai trò "Kẻ Điều Phối" (Supervisor) giám sát và cấy ghép trí não vào "Bác sĩ Phẫu thuật" (Antigravity IDE) qua giao thức CDP.
|
|
17
18
|
- **Luật Cấm Kị (Anti-Hallucination):** Tuyệt đối không được dùng tư duy lập trình phổ quát máy móc để sửa dự án này. Mọi quyết định sửa mã liên quan đến CDP, luồng Telegram, hay Ports mạng, CẦN chiếu theo các file `SKILL.md` lân cận (`telegram-agentic-gateway`, `remoat-integration`, `llm-routing-quirks`).
|
|
18
19
|
|
|
@@ -45,6 +46,76 @@ Phải cực kỳ tỉnh táo để nhận diện loại kiến thức trước
|
|
|
45
46
|
2. **Kiến trúc Lõi (AG-KIT Framework):** Nếu logic bạn vừa sửa là kiến trúc lõi dùng chung cho mọi con Bot/Agent khác về sau (Ví dụ: Định tuyến LLM, Telegram Gateway, Cơ chế Dual-Ports, Lỗi đứt cáp CDP) $\rightarrow$ Bạn **BẮT BUỘC phải ghi vào `.agent/skills/`** và đóng gói code chuẩn mực vào thư mục `templates/` của ag-kit.
|
|
46
47
|
*Bê logic riêng của dự án nhét vào ag-kit là tội đồ làm ô nhiễm Framework.*
|
|
47
48
|
|
|
49
|
+
### 3.1 🛡️ LUẬT CẤM "CHẮP VÁ" (NO FRAGMENTED UPDATES)
|
|
50
|
+
|
|
51
|
+
**Đây là quy tắc tối cao để giữ Giao diện Trí tuệ của Agent luôn chuyên nghiệp và logic.**
|
|
52
|
+
|
|
53
|
+
- **Cảnh báo Số thứ tự (Index Integrity):** Cấm tuyệt đối việc cập nhật nhảy cóc số thứ tự (Ví dụ: 1, 2, 3 -> 5). Mọi danh sách phải được AI rà soát lại từ đầu đến cuối trước khi ghi file để đảm bảo tính tuần tự (1, 2, 3, 4, 5...).
|
|
54
|
+
- **Cảnh báo Chỗ trống (Structural Gap):** Không được để lại các đề mục rỗng hoặc các chú thích "Sẽ cập nhật sau".
|
|
55
|
+
- **Hành động bắt buộc:** Trước khi thực hiện `multi_replace_file_content` hay `replace_file_content`, Agent phải thực hiện một bước "Visual Audit" (Đọc lại toàn bộ nội dung file) để đảm bảo các thay đổi mới không làm gãy cấu trúc cũ hoặc gây lộn xộn nội dung. *Mọi sự cẩu thả trong trình bày tri thức sẽ bị coi là lỗi hệ thống.*
|
|
56
|
+
|
|
57
|
+
### 3.2 🧠 BẢN ĐỒ 3 TẦNG TRÍ NHỚ (THE 3-TIER MEMORY MODEL — MANDATORY)
|
|
58
|
+
|
|
59
|
+
**Mọi Agent vận hành trên ag-kit PHẢI hiểu và lưu đúng tầng:**
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
63
|
+
│ TẦNG 3 — KNOWLEDGE BASE (Sự Thật Vĩnh Cửu) │
|
|
64
|
+
│ Lưu ở: .agent/knowledge/*.md │
|
|
65
|
+
│ Nội dung: kiến trúc, patterns, quy tắc đã được duyệt │
|
|
66
|
+
│ ✅ Git-tracked ✅ Con người đọc được ✅ Cross-session │
|
|
67
|
+
├─────────────────────────────────────────────────────────────┤
|
|
68
|
+
│ TẦNG 2 — WORKING MEMORY GRAPH (Ký Ức Động) │
|
|
69
|
+
│ Lưu ở: .agent/memory/graph.db (SQLite) │
|
|
70
|
+
│ Nội dung: error solutions, decisions, context sự kiện │
|
|
71
|
+
│ Tool: python3 .agent/scripts/memory_tool.py │
|
|
72
|
+
│ ⚠️ .gitignore ✅ Auto-GC (Energy decay) ✅ Persistent │
|
|
73
|
+
├─────────────────────────────────────────────────────────────┤
|
|
74
|
+
│ TẦNG 1 — CODEBASE INDEX (Đôi Mắt) │
|
|
75
|
+
│ Lưu ở: .agent/cache/repomap.json │
|
|
76
|
+
│ Nội dung: sơ đồ function/class của toàn codebase │
|
|
77
|
+
│ Rebuild: bash .agent/scripts/refresh_brain.sh │
|
|
78
|
+
│ ⚠️ .gitignore ✅ Auto-rebuild khi code đổi ✅ Fast │
|
|
79
|
+
└─────────────────────────────────────────────────────────────┘
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Quy tắc định tuyến (Routing Rules):**
|
|
83
|
+
| Loại thông tin | Lưu vào Tầng |
|
|
84
|
+
|---|---|
|
|
85
|
+
| Kiến trúc, quy tắc vận hành đã xác nhận | Tầng 3 (`.md`) |
|
|
86
|
+
| Bug đã fix, quyết định ngắn hạn, context phiên | Tầng 2 (`graph.db`) |
|
|
87
|
+
| Cấu trúc code, danh sách hàm | Tầng 1 (`repomap.json`) |
|
|
88
|
+
| Trọng số sống, dữ liệu thời gian thực | Biệt lập (`brain_v1.json` + `.gitignore`) |
|
|
89
|
+
|
|
90
|
+
**Auto-Brain (Tổng hợp tự động — KHÔNG CẦN GÕ LỆNH):**
|
|
91
|
+
- Mỗi project có `.agent/brain/summary.md` — bộ não tổng hợp từ cả 3 tầng trên.
|
|
92
|
+
- Tự động rebuild sau mỗi `git commit` (hook) và khi khởi động session.
|
|
93
|
+
- Tool: `python3 .agent/scripts/brain_builder.py`
|
|
94
|
+
- Global registry: `~/.config/ag-kit/projects.json` — danh bạ tất cả projects.
|
|
95
|
+
|
|
96
|
+
### 3.3 🧠 GIAO THỨC MCP MEMORY (MANDATORY)
|
|
97
|
+
|
|
98
|
+
**BẮT BUỘC: AI phải gọi MCP tools, KHÔNG được dùng shell command để truy cập memory.**
|
|
99
|
+
|
|
100
|
+
Server `ag-kit-memory` được đăng ký sẵn trong `~/.gemini/antigravity/mcp_config.json`. AI gọi trực tiếp:
|
|
101
|
+
|
|
102
|
+
| Tool | Khi nào gọi |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `memory_search(keyword)` | **TRƯỚC** khi bắt đầu bất kỳ task nào — kiểm tra context cũ |
|
|
105
|
+
| `memory_save(content, category)` | **SAU** khi fix bug, ra quyết định, học được điều mới |
|
|
106
|
+
| `memory_link(from, to, relation)` | Khi phát hiện 2 kiến thức có quan hệ |
|
|
107
|
+
| `memory_consolidate(days=7)` | Hàng tuần: gộp hot → cold summaries |
|
|
108
|
+
| `memory_status()` | Kiểm tra sức khỏe của bộ não |
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
Workflow đúng:
|
|
112
|
+
1. Bắt đầu task → memory_search("topic") → đọc context cũ
|
|
113
|
+
2. Làm việc...
|
|
114
|
+
3. Xong task → memory_save("kết quả/quyết định quan trịng")
|
|
115
|
+
4. Hàng tuần → memory_consolidate(days=7) → dọn rác, tổng hợp
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
|
|
48
119
|
---
|
|
49
120
|
|
|
50
121
|
## 4. 🔗 THE DUO-CORE (Kiến Trúc Multi-Tenant Bắt Buộc)
|
|
@@ -52,7 +123,22 @@ Hãy khắc cốt ghi tâm quy tắc: `IDE_PORT` (Nhận lệnh AI - ví dụ: 9
|
|
|
52
123
|
|
|
53
124
|
---
|
|
54
125
|
|
|
55
|
-
## 5.
|
|
126
|
+
## 5. 🌐 TẦM NHÌN XUYÊN DỰ ÁN VÀ TỔNG QUÁT HÓA (CROSS-PROJECT VISION & GENERALIZATION)
|
|
127
|
+
|
|
128
|
+
**ĐÂY LÀ QUY TẮC ĐỂ TRÁNH "NHIỄM ĐỘC NGỮ CẢNH" (CONTEXT CONTAMINATION).**
|
|
129
|
+
|
|
130
|
+
- **Quyền quan sát (Workspace as Library):** Bạn ĐƯỢC PHÉP "soi" sang các thư mục khác trong `/home/tao/Projects/` để nghiên cứu giải pháp kiến trúc.
|
|
131
|
+
- **Giao thức đọc bộ não dự án khác (Cross-Project Brain Reading):**
|
|
132
|
+
1. Kiểm tra `~/.config/ag-kit/projects.json` để tìm tên Project cần xem.
|
|
133
|
+
2. Đọc `<project_root>/.agent/brain/summary.md` (một file duy nhất, ~500 tokens).
|
|
134
|
+
3. Nếu cần đi sâu hơn: dùng `vfs <project_root>/src` hoặc `memory_tool.py search`.
|
|
135
|
+
4. **CẤM** scan toàn bộ thư mục project khác khi chưa đọc `summary.md` trước.
|
|
136
|
+
- **Cấm ô nhiễm (No Leakage):** Tuyệt đối không ghi chép các từ khóa chuyên biệt (B1, B2, Sentinel, DNSE...) vào các file Skill dùng chung.
|
|
137
|
+
- **Tổng quát hóa (Sanitization):** Mọi pattern học được từ project A khi đưa vào Core Framework đều phải tẩy sạch tên nghiệp vụ. Trước khi trích xuất pattern lớn, hỏi Sếp: *"Em thấy pattern X ở dự án Y rất hay, em xin phép tổng quát hóa nó không?"*
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 6. 🛠️ HỆ THỐNG CLI CỦA AG-KIT (BẢN ĐỒ CÔNG CỤ)
|
|
56
142
|
Hệ sinh thái `ag-kit` được cung cấp sức mạnh cài đặt và cập nhật thông qua 2 bộ công cụ trên NPM. Bất kỳ AI nào khi làm việc cũng phải hiểu rõ điểm khác biệt và biết cách hướng dẫn User:
|
|
57
143
|
|
|
58
144
|
1. **Bộ Cập Nhật & Quản Trị Tương Tác (@mrtrinhvn/ag-kit):**
|