agmem 0.1.1__py3-none-any.whl → 0.1.3__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.
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/METADATA +157 -16
- agmem-0.1.3.dist-info/RECORD +105 -0
- memvcs/__init__.py +1 -1
- memvcs/cli.py +45 -31
- memvcs/commands/__init__.py +9 -9
- memvcs/commands/add.py +83 -76
- memvcs/commands/audit.py +59 -0
- memvcs/commands/blame.py +46 -53
- memvcs/commands/branch.py +13 -33
- memvcs/commands/checkout.py +27 -32
- memvcs/commands/clean.py +18 -23
- memvcs/commands/clone.py +11 -1
- memvcs/commands/commit.py +40 -39
- memvcs/commands/daemon.py +109 -76
- memvcs/commands/decay.py +77 -0
- memvcs/commands/diff.py +56 -57
- memvcs/commands/distill.py +90 -0
- memvcs/commands/federated.py +53 -0
- memvcs/commands/fsck.py +86 -61
- memvcs/commands/garden.py +40 -35
- memvcs/commands/gc.py +51 -0
- memvcs/commands/graph.py +41 -48
- memvcs/commands/init.py +16 -24
- memvcs/commands/log.py +25 -40
- memvcs/commands/merge.py +69 -27
- memvcs/commands/pack.py +129 -0
- memvcs/commands/prove.py +66 -0
- memvcs/commands/pull.py +31 -1
- memvcs/commands/push.py +4 -2
- memvcs/commands/recall.py +145 -0
- memvcs/commands/reflog.py +13 -22
- memvcs/commands/remote.py +1 -0
- memvcs/commands/repair.py +66 -0
- memvcs/commands/reset.py +23 -33
- memvcs/commands/resolve.py +130 -0
- memvcs/commands/resurrect.py +82 -0
- memvcs/commands/search.py +3 -4
- memvcs/commands/serve.py +2 -1
- memvcs/commands/show.py +66 -36
- memvcs/commands/stash.py +34 -34
- memvcs/commands/status.py +27 -35
- memvcs/commands/tag.py +23 -47
- memvcs/commands/test.py +30 -44
- memvcs/commands/timeline.py +111 -0
- memvcs/commands/tree.py +26 -27
- memvcs/commands/verify.py +110 -0
- memvcs/commands/when.py +115 -0
- memvcs/core/access_index.py +167 -0
- memvcs/core/audit.py +124 -0
- memvcs/core/config_loader.py +3 -1
- memvcs/core/consistency.py +214 -0
- memvcs/core/crypto_verify.py +280 -0
- memvcs/core/decay.py +185 -0
- memvcs/core/diff.py +158 -143
- memvcs/core/distiller.py +277 -0
- memvcs/core/encryption.py +169 -0
- memvcs/core/federated.py +86 -0
- memvcs/core/gardener.py +176 -145
- memvcs/core/hooks.py +48 -14
- memvcs/core/ipfs_remote.py +39 -0
- memvcs/core/knowledge_graph.py +135 -138
- memvcs/core/llm/__init__.py +10 -0
- memvcs/core/llm/anthropic_provider.py +50 -0
- memvcs/core/llm/base.py +27 -0
- memvcs/core/llm/factory.py +30 -0
- memvcs/core/llm/openai_provider.py +36 -0
- memvcs/core/merge.py +260 -170
- memvcs/core/objects.py +110 -101
- memvcs/core/pack.py +92 -0
- memvcs/core/pii_scanner.py +147 -146
- memvcs/core/privacy_budget.py +63 -0
- memvcs/core/refs.py +132 -115
- memvcs/core/remote.py +38 -0
- memvcs/core/repository.py +254 -164
- memvcs/core/schema.py +155 -113
- memvcs/core/staging.py +60 -65
- memvcs/core/storage/__init__.py +20 -18
- memvcs/core/storage/base.py +74 -70
- memvcs/core/storage/gcs.py +70 -68
- memvcs/core/storage/local.py +42 -40
- memvcs/core/storage/s3.py +105 -110
- memvcs/core/temporal_index.py +121 -0
- memvcs/core/test_runner.py +101 -93
- memvcs/core/trust.py +103 -0
- memvcs/core/vector_store.py +56 -36
- memvcs/core/zk_proofs.py +26 -0
- memvcs/integrations/mcp_server.py +1 -3
- memvcs/integrations/web_ui/server.py +25 -26
- memvcs/retrieval/__init__.py +22 -0
- memvcs/retrieval/base.py +54 -0
- memvcs/retrieval/pack.py +128 -0
- memvcs/retrieval/recaller.py +105 -0
- memvcs/retrieval/strategies.py +314 -0
- memvcs/utils/__init__.py +3 -3
- memvcs/utils/helpers.py +52 -52
- agmem-0.1.1.dist-info/RECORD +0 -67
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/WHEEL +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/entry_points.txt +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/top_level.txt +0 -0
memvcs/commands/graph.py
CHANGED
|
@@ -11,109 +11,102 @@ from ..commands.base import require_repo
|
|
|
11
11
|
|
|
12
12
|
class GraphCommand:
|
|
13
13
|
"""Visualize connections between memory files."""
|
|
14
|
-
|
|
15
|
-
name =
|
|
16
|
-
help =
|
|
17
|
-
|
|
14
|
+
|
|
15
|
+
name = "graph"
|
|
16
|
+
help = "Visualize the knowledge graph of memory files"
|
|
17
|
+
|
|
18
18
|
@staticmethod
|
|
19
19
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
20
|
+
parser.add_argument("--output", "-o", help="Output file for graph data (JSON)")
|
|
20
21
|
parser.add_argument(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
'--format',
|
|
26
|
-
choices=['json', 'd3', 'summary'],
|
|
27
|
-
default='summary',
|
|
28
|
-
help='Output format (default: summary)'
|
|
22
|
+
"--format",
|
|
23
|
+
choices=["json", "d3", "summary"],
|
|
24
|
+
default="summary",
|
|
25
|
+
help="Output format (default: summary)",
|
|
29
26
|
)
|
|
30
27
|
parser.add_argument(
|
|
31
|
-
|
|
32
|
-
action='store_true',
|
|
33
|
-
help='Skip similarity-based edges (faster)'
|
|
28
|
+
"--no-similarity", action="store_true", help="Skip similarity-based edges (faster)"
|
|
34
29
|
)
|
|
35
30
|
parser.add_argument(
|
|
36
|
-
|
|
31
|
+
"--threshold",
|
|
37
32
|
type=float,
|
|
38
33
|
default=0.7,
|
|
39
|
-
help=
|
|
34
|
+
help="Similarity threshold for edges (default: 0.7)",
|
|
40
35
|
)
|
|
41
36
|
parser.add_argument(
|
|
42
|
-
|
|
43
|
-
action='store_true',
|
|
44
|
-
help='Start web server to view interactive graph'
|
|
37
|
+
"--serve", action="store_true", help="Start web server to view interactive graph"
|
|
45
38
|
)
|
|
46
|
-
|
|
39
|
+
|
|
47
40
|
@staticmethod
|
|
48
41
|
def execute(args) -> int:
|
|
49
42
|
repo, code = require_repo()
|
|
50
43
|
if code != 0:
|
|
51
44
|
return code
|
|
52
|
-
|
|
45
|
+
|
|
53
46
|
# Try to get vector store for similarity
|
|
54
47
|
vector_store = None
|
|
55
48
|
if not args.no_similarity:
|
|
56
49
|
try:
|
|
57
50
|
from ..core.vector_store import VectorStore
|
|
58
|
-
|
|
51
|
+
|
|
52
|
+
vector_store = VectorStore(repo.root / ".mem")
|
|
59
53
|
except ImportError:
|
|
60
54
|
print("Note: Vector store not available, skipping similarity edges")
|
|
61
|
-
|
|
55
|
+
|
|
62
56
|
# Build graph
|
|
63
57
|
from ..core.knowledge_graph import KnowledgeGraphBuilder
|
|
64
|
-
|
|
58
|
+
|
|
65
59
|
builder = KnowledgeGraphBuilder(repo, vector_store)
|
|
66
|
-
|
|
60
|
+
|
|
67
61
|
print("Building knowledge graph...")
|
|
68
62
|
graph_data = builder.build_graph(
|
|
69
|
-
include_similarity=not args.no_similarity,
|
|
70
|
-
similarity_threshold=args.threshold
|
|
63
|
+
include_similarity=not args.no_similarity, similarity_threshold=args.threshold
|
|
71
64
|
)
|
|
72
|
-
|
|
65
|
+
|
|
73
66
|
if args.serve:
|
|
74
67
|
return GraphCommand._serve_graph(repo, graph_data)
|
|
75
|
-
|
|
76
|
-
if args.format ==
|
|
68
|
+
|
|
69
|
+
if args.format == "summary":
|
|
77
70
|
GraphCommand._print_summary(graph_data, builder)
|
|
78
|
-
|
|
79
|
-
elif args.format ==
|
|
71
|
+
|
|
72
|
+
elif args.format == "json":
|
|
80
73
|
output = graph_data.to_json()
|
|
81
74
|
if args.output:
|
|
82
75
|
Path(args.output).write_text(output)
|
|
83
76
|
print(f"Graph data written to: {args.output}")
|
|
84
77
|
else:
|
|
85
78
|
print(output)
|
|
86
|
-
|
|
87
|
-
elif args.format ==
|
|
79
|
+
|
|
80
|
+
elif args.format == "d3":
|
|
88
81
|
output = builder.export_for_d3()
|
|
89
82
|
if args.output:
|
|
90
83
|
Path(args.output).write_text(output)
|
|
91
84
|
print(f"D3 graph data written to: {args.output}")
|
|
92
85
|
else:
|
|
93
86
|
print(output)
|
|
94
|
-
|
|
87
|
+
|
|
95
88
|
return 0
|
|
96
|
-
|
|
89
|
+
|
|
97
90
|
@staticmethod
|
|
98
91
|
def _print_summary(graph_data, builder):
|
|
99
92
|
"""Print a text summary of the graph."""
|
|
100
93
|
meta = graph_data.metadata
|
|
101
|
-
|
|
94
|
+
|
|
102
95
|
print("\nKnowledge Graph Summary")
|
|
103
96
|
print("=" * 40)
|
|
104
97
|
print(f"Total files: {meta['total_nodes']}")
|
|
105
98
|
print(f"Total connections: {meta['total_edges']}")
|
|
106
|
-
|
|
99
|
+
|
|
107
100
|
print("\nBy Memory Type:")
|
|
108
|
-
for mtype, count in meta[
|
|
101
|
+
for mtype, count in meta["memory_types"].items():
|
|
109
102
|
if count > 0:
|
|
110
103
|
print(f" {mtype}: {count}")
|
|
111
|
-
|
|
104
|
+
|
|
112
105
|
print("\nBy Edge Type:")
|
|
113
|
-
for etype, count in meta[
|
|
106
|
+
for etype, count in meta["edge_types"].items():
|
|
114
107
|
if count > 0:
|
|
115
108
|
print(f" {etype}: {count}")
|
|
116
|
-
|
|
109
|
+
|
|
117
110
|
# Find isolated nodes
|
|
118
111
|
isolated = builder.find_isolated_nodes()
|
|
119
112
|
if isolated:
|
|
@@ -122,16 +115,16 @@ class GraphCommand:
|
|
|
122
115
|
print(f" - {path}")
|
|
123
116
|
if len(isolated) > 5:
|
|
124
117
|
print(f" ... and {len(isolated) - 5} more")
|
|
125
|
-
|
|
118
|
+
|
|
126
119
|
# Find potential contradictions
|
|
127
120
|
contradictions = builder.find_potential_contradictions()
|
|
128
121
|
if contradictions:
|
|
129
122
|
print(f"\nPotential contradictions: {len(contradictions)}")
|
|
130
123
|
for path1, path2, sim in contradictions[:3]:
|
|
131
124
|
print(f" - {path1} <-> {path2} (similarity: {sim:.2%})")
|
|
132
|
-
|
|
125
|
+
|
|
133
126
|
print("\nUse --format d3 --output graph.json to export for visualization")
|
|
134
|
-
|
|
127
|
+
|
|
135
128
|
@staticmethod
|
|
136
129
|
def _serve_graph(repo, graph_data):
|
|
137
130
|
"""Start web server to view interactive graph."""
|
|
@@ -142,10 +135,10 @@ class GraphCommand:
|
|
|
142
135
|
print("Error: Web server requires fastapi and uvicorn.")
|
|
143
136
|
print("Install with: pip install agmem[web]")
|
|
144
137
|
return 1
|
|
145
|
-
|
|
138
|
+
|
|
146
139
|
print("Starting graph visualization server...")
|
|
147
140
|
print("Open http://localhost:8080/graph in your browser")
|
|
148
|
-
|
|
141
|
+
|
|
149
142
|
app = create_app(repo.root)
|
|
150
143
|
uvicorn.run(app, host="127.0.0.1", port=8080)
|
|
151
144
|
return 0
|
memvcs/commands/init.py
CHANGED
|
@@ -10,49 +10,41 @@ from ..core.repository import Repository
|
|
|
10
10
|
|
|
11
11
|
class InitCommand:
|
|
12
12
|
"""Initialize a new agmem repository."""
|
|
13
|
-
|
|
14
|
-
name =
|
|
15
|
-
help =
|
|
16
|
-
|
|
13
|
+
|
|
14
|
+
name = "init"
|
|
15
|
+
help = "Initialize a new memory repository"
|
|
16
|
+
|
|
17
17
|
@staticmethod
|
|
18
18
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
19
19
|
parser.add_argument(
|
|
20
|
-
|
|
21
|
-
nargs=
|
|
22
|
-
default=
|
|
23
|
-
help=
|
|
20
|
+
"path",
|
|
21
|
+
nargs="?",
|
|
22
|
+
default=".",
|
|
23
|
+
help="Directory to initialize repository in (default: current directory)",
|
|
24
24
|
)
|
|
25
|
+
parser.add_argument("--author-name", default="Agent", help="Default author name")
|
|
25
26
|
parser.add_argument(
|
|
26
|
-
|
|
27
|
-
default='Agent',
|
|
28
|
-
help='Default author name'
|
|
27
|
+
"--author-email", default="agent@example.com", help="Default author email"
|
|
29
28
|
)
|
|
30
|
-
|
|
31
|
-
'--author-email',
|
|
32
|
-
default='agent@example.com',
|
|
33
|
-
help='Default author email'
|
|
34
|
-
)
|
|
35
|
-
|
|
29
|
+
|
|
36
30
|
@staticmethod
|
|
37
31
|
def execute(args) -> int:
|
|
38
32
|
path = Path(args.path).resolve()
|
|
39
|
-
|
|
33
|
+
|
|
40
34
|
try:
|
|
41
35
|
repo = Repository.init(
|
|
42
|
-
path=path,
|
|
43
|
-
author_name=args.author_name,
|
|
44
|
-
author_email=args.author_email
|
|
36
|
+
path=path, author_name=args.author_name, author_email=args.author_email
|
|
45
37
|
)
|
|
46
|
-
|
|
38
|
+
|
|
47
39
|
print(f"Initialized empty agmem repository in {repo.mem_dir}")
|
|
48
40
|
print(f"Author: {args.author_name} <{args.author_email}>")
|
|
49
41
|
print(f"\nNext steps:")
|
|
50
42
|
print(f" 1. Add memory files to {repo.current_dir}/")
|
|
51
43
|
print(f" 2. Run 'agmem add <file>' to stage changes")
|
|
52
44
|
print(f" 3. Run 'agmem commit -m \"message\"' to save snapshot")
|
|
53
|
-
|
|
45
|
+
|
|
54
46
|
return 0
|
|
55
|
-
|
|
47
|
+
|
|
56
48
|
except ValueError as e:
|
|
57
49
|
print(f"Error: {e}")
|
|
58
50
|
return 1
|
memvcs/commands/log.py
CHANGED
|
@@ -11,39 +11,24 @@ from ..core.repository import Repository
|
|
|
11
11
|
|
|
12
12
|
class LogCommand:
|
|
13
13
|
"""Show commit history."""
|
|
14
|
-
|
|
15
|
-
name =
|
|
16
|
-
help =
|
|
17
|
-
|
|
14
|
+
|
|
15
|
+
name = "log"
|
|
16
|
+
help = "Show commit history"
|
|
17
|
+
|
|
18
18
|
@staticmethod
|
|
19
19
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
20
20
|
parser.add_argument(
|
|
21
|
-
|
|
22
|
-
type=int,
|
|
23
|
-
default=10,
|
|
24
|
-
help='Maximum number of commits to show'
|
|
25
|
-
)
|
|
26
|
-
parser.add_argument(
|
|
27
|
-
'--oneline',
|
|
28
|
-
action='store_true',
|
|
29
|
-
help='Show one commit per line'
|
|
21
|
+
"--max-count", "-n", type=int, default=10, help="Maximum number of commits to show"
|
|
30
22
|
)
|
|
23
|
+
parser.add_argument("--oneline", action="store_true", help="Show one commit per line")
|
|
31
24
|
parser.add_argument(
|
|
32
|
-
|
|
33
|
-
action='store_true',
|
|
34
|
-
help='Show ASCII graph of branch/merge history'
|
|
25
|
+
"--graph", action="store_true", help="Show ASCII graph of branch/merge history"
|
|
35
26
|
)
|
|
27
|
+
parser.add_argument("--all", action="store_true", help="Show all branches")
|
|
36
28
|
parser.add_argument(
|
|
37
|
-
|
|
38
|
-
action='store_true',
|
|
39
|
-
help='Show all branches'
|
|
29
|
+
"ref", nargs="?", help="Start from this reference (branch, tag, or commit)"
|
|
40
30
|
)
|
|
41
|
-
|
|
42
|
-
'ref',
|
|
43
|
-
nargs='?',
|
|
44
|
-
help='Start from this reference (branch, tag, or commit)'
|
|
45
|
-
)
|
|
46
|
-
|
|
31
|
+
|
|
47
32
|
@staticmethod
|
|
48
33
|
def execute(args) -> int:
|
|
49
34
|
repo, code = require_repo()
|
|
@@ -52,11 +37,11 @@ class LogCommand:
|
|
|
52
37
|
|
|
53
38
|
# Get commits
|
|
54
39
|
commits = repo.get_log(max_count=args.max_count)
|
|
55
|
-
|
|
40
|
+
|
|
56
41
|
if not commits:
|
|
57
42
|
print("No commits yet.")
|
|
58
43
|
return 0
|
|
59
|
-
|
|
44
|
+
|
|
60
45
|
if args.oneline:
|
|
61
46
|
for commit in commits:
|
|
62
47
|
print(f"{commit['short_hash']} {commit['message']}")
|
|
@@ -71,33 +56,33 @@ class LogCommand:
|
|
|
71
56
|
for i, commit in enumerate(commits):
|
|
72
57
|
if i > 0:
|
|
73
58
|
print()
|
|
74
|
-
|
|
59
|
+
|
|
75
60
|
# Commit header
|
|
76
61
|
print(f"\033[33mcommit {commit['hash']}\033[0m")
|
|
77
|
-
|
|
62
|
+
|
|
78
63
|
# Show branch info if this is HEAD
|
|
79
64
|
head = repo.refs.get_head()
|
|
80
|
-
if head[
|
|
81
|
-
head_commit = repo.refs.get_branch_commit(head[
|
|
82
|
-
if head_commit == commit[
|
|
65
|
+
if head["type"] == "branch":
|
|
66
|
+
head_commit = repo.refs.get_branch_commit(head["value"])
|
|
67
|
+
if head_commit == commit["hash"]:
|
|
83
68
|
print(f"\033[36mHEAD -> {head['value']}\033[0m")
|
|
84
|
-
|
|
69
|
+
|
|
85
70
|
# Author and date
|
|
86
71
|
print(f"Author: {commit['author']}")
|
|
87
|
-
|
|
72
|
+
|
|
88
73
|
# Format timestamp
|
|
89
74
|
try:
|
|
90
|
-
ts = commit[
|
|
91
|
-
if ts.endswith(
|
|
75
|
+
ts = commit["timestamp"]
|
|
76
|
+
if ts.endswith("Z"):
|
|
92
77
|
ts = ts[:-1]
|
|
93
78
|
dt = datetime.fromisoformat(ts)
|
|
94
|
-
date_str = dt.strftime(
|
|
79
|
+
date_str = dt.strftime("%a %b %d %H:%M:%S %Y")
|
|
95
80
|
print(f"Date: {date_str}")
|
|
96
|
-
except:
|
|
81
|
+
except Exception:
|
|
97
82
|
print(f"Date: {commit['timestamp']}")
|
|
98
|
-
|
|
83
|
+
|
|
99
84
|
# Message
|
|
100
85
|
print()
|
|
101
86
|
print(f" {commit['message']}")
|
|
102
|
-
|
|
87
|
+
|
|
103
88
|
return 0
|
memvcs/commands/merge.py
CHANGED
|
@@ -11,31 +11,24 @@ from ..core.repository import Repository
|
|
|
11
11
|
|
|
12
12
|
class MergeCommand:
|
|
13
13
|
"""Merge branches."""
|
|
14
|
-
|
|
15
|
-
name =
|
|
16
|
-
help =
|
|
17
|
-
|
|
14
|
+
|
|
15
|
+
name = "merge"
|
|
16
|
+
help = "Join two or more development histories together"
|
|
17
|
+
|
|
18
18
|
@staticmethod
|
|
19
19
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
20
|
+
parser.add_argument("branch", help="Branch to merge into current branch")
|
|
21
|
+
parser.add_argument("-m", "--message", help="Merge commit message")
|
|
20
22
|
parser.add_argument(
|
|
21
|
-
|
|
22
|
-
help='Branch to merge into current branch'
|
|
23
|
+
"--no-commit", action="store_true", help="Perform merge but do not commit"
|
|
23
24
|
)
|
|
25
|
+
parser.add_argument("--abort", action="store_true", help="Abort the current merge")
|
|
24
26
|
parser.add_argument(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
"--yes",
|
|
28
|
+
action="store_true",
|
|
29
|
+
help="Accept conditionally trusted branch commits without prompting",
|
|
27
30
|
)
|
|
28
|
-
|
|
29
|
-
'--no-commit',
|
|
30
|
-
action='store_true',
|
|
31
|
-
help='Perform merge but do not commit'
|
|
32
|
-
)
|
|
33
|
-
parser.add_argument(
|
|
34
|
-
'--abort',
|
|
35
|
-
action='store_true',
|
|
36
|
-
help='Abort the current merge'
|
|
37
|
-
)
|
|
38
|
-
|
|
31
|
+
|
|
39
32
|
@staticmethod
|
|
40
33
|
def execute(args) -> int:
|
|
41
34
|
repo, code = require_repo()
|
|
@@ -47,42 +40,91 @@ class MergeCommand:
|
|
|
47
40
|
# TODO: Implement merge abort
|
|
48
41
|
print("Merge abort not yet implemented")
|
|
49
42
|
return 0
|
|
50
|
-
|
|
43
|
+
|
|
51
44
|
# Check if we're on a branch
|
|
52
45
|
current_branch = repo.refs.get_current_branch()
|
|
53
46
|
if not current_branch:
|
|
54
47
|
print("Error: Not currently on any branch.")
|
|
55
48
|
print("Cannot merge when HEAD is detached.")
|
|
56
49
|
return 1
|
|
57
|
-
|
|
50
|
+
|
|
58
51
|
# Check if trying to merge current branch
|
|
59
52
|
if args.branch == current_branch:
|
|
60
53
|
print(f"Error: Cannot merge '{args.branch}' into itself")
|
|
61
54
|
return 1
|
|
62
|
-
|
|
55
|
+
|
|
63
56
|
# Check if branch exists
|
|
64
57
|
if not repo.refs.branch_exists(args.branch):
|
|
65
58
|
print(f"Error: Branch '{args.branch}' not found.")
|
|
66
59
|
return 1
|
|
67
|
-
|
|
60
|
+
|
|
61
|
+
# Trust check for branch tip (may be from another agent)
|
|
62
|
+
other_commit_hash = repo.refs.get_branch_commit(args.branch)
|
|
63
|
+
if other_commit_hash:
|
|
64
|
+
from ..core.objects import Commit
|
|
65
|
+
from ..core.trust import find_verifying_key, get_trust_level
|
|
66
|
+
|
|
67
|
+
other_commit = Commit.load(repo.object_store, other_commit_hash)
|
|
68
|
+
if other_commit and other_commit.metadata:
|
|
69
|
+
key_pem = find_verifying_key(repo.mem_dir, other_commit.metadata)
|
|
70
|
+
if key_pem is not None:
|
|
71
|
+
level = get_trust_level(repo.mem_dir, key_pem)
|
|
72
|
+
if level == "untrusted":
|
|
73
|
+
print(f"Merge blocked: branch '{args.branch}' signed by untrusted key.")
|
|
74
|
+
return 1
|
|
75
|
+
if level == "conditional" and not getattr(args, "yes", False):
|
|
76
|
+
print("Branch signed by conditionally trusted key. Use --yes to merge.")
|
|
77
|
+
return 1
|
|
78
|
+
|
|
68
79
|
# Perform merge
|
|
69
80
|
engine = MergeEngine(repo)
|
|
70
81
|
result = engine.merge(args.branch, message=args.message)
|
|
71
|
-
|
|
82
|
+
|
|
72
83
|
if result.success:
|
|
73
84
|
print(f"Merge successful: {result.message}")
|
|
74
85
|
if result.commit_hash:
|
|
75
86
|
print(f" Commit: {result.commit_hash[:8]}")
|
|
87
|
+
try:
|
|
88
|
+
from ..core.audit import append_audit
|
|
89
|
+
|
|
90
|
+
append_audit(
|
|
91
|
+
repo.mem_dir, "merge", {"branch": args.branch, "commit": result.commit_hash}
|
|
92
|
+
)
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
76
95
|
return 0
|
|
77
96
|
else:
|
|
78
97
|
print(f"Merge failed: {result.message}")
|
|
79
|
-
|
|
98
|
+
|
|
80
99
|
if result.conflicts:
|
|
100
|
+
# Persist conflicts for agmem resolve
|
|
101
|
+
try:
|
|
102
|
+
import json
|
|
103
|
+
|
|
104
|
+
merge_dir = repo.mem_dir / "merge"
|
|
105
|
+
merge_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
conflicts_data = [
|
|
107
|
+
{
|
|
108
|
+
"path": c.path,
|
|
109
|
+
"message": c.message,
|
|
110
|
+
"memory_type": getattr(c, "memory_type", None),
|
|
111
|
+
"payload": getattr(c, "payload", None),
|
|
112
|
+
"ours_content": c.ours_content,
|
|
113
|
+
"theirs_content": c.theirs_content,
|
|
114
|
+
"base_content": c.base_content,
|
|
115
|
+
}
|
|
116
|
+
for c in result.conflicts
|
|
117
|
+
]
|
|
118
|
+
(merge_dir / "conflicts.json").write_text(json.dumps(conflicts_data, indent=2))
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
81
121
|
print()
|
|
82
122
|
print("Conflicts:")
|
|
83
123
|
for conflict in result.conflicts:
|
|
84
124
|
print(f" {conflict.path}")
|
|
85
125
|
print()
|
|
86
|
-
print(
|
|
87
|
-
|
|
126
|
+
print(
|
|
127
|
+
"Resolve conflicts with 'agmem resolve' or edit files and run 'agmem commit'."
|
|
128
|
+
)
|
|
129
|
+
|
|
88
130
|
return 1
|
memvcs/commands/pack.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem pack - Context window budget manager.
|
|
3
|
+
|
|
4
|
+
Packs recalled memories into token budget for LLM injection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ..commands.base import require_repo
|
|
12
|
+
from ..core.access_index import AccessIndex
|
|
13
|
+
from ..retrieval import RecallEngine
|
|
14
|
+
from ..retrieval.pack import PackEngine
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PackCommand:
|
|
18
|
+
"""Context window budget manager."""
|
|
19
|
+
|
|
20
|
+
name = "pack"
|
|
21
|
+
help = "Pack recalled memories into token budget for LLM context"
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--context",
|
|
27
|
+
"-c",
|
|
28
|
+
default="",
|
|
29
|
+
help="Current task description for recall",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--budget",
|
|
33
|
+
"-b",
|
|
34
|
+
type=int,
|
|
35
|
+
default=4000,
|
|
36
|
+
help="Max tokens (default: 4000)",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--strategy",
|
|
40
|
+
"-s",
|
|
41
|
+
choices=["relevance", "recency", "importance", "balanced"],
|
|
42
|
+
default="relevance",
|
|
43
|
+
help="Packing strategy (default: relevance)",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--exclude",
|
|
47
|
+
"-e",
|
|
48
|
+
action="append",
|
|
49
|
+
default=[],
|
|
50
|
+
help="Paths to exclude; repeatable",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--model",
|
|
54
|
+
"-m",
|
|
55
|
+
default="gpt-4o-mini",
|
|
56
|
+
help="Model for token counting (default: gpt-4o-mini)",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--format",
|
|
60
|
+
"-f",
|
|
61
|
+
choices=["text", "json"],
|
|
62
|
+
default="text",
|
|
63
|
+
help="Output format (default: text)",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def execute(args) -> int:
|
|
68
|
+
repo, code = require_repo()
|
|
69
|
+
if code != 0:
|
|
70
|
+
return code
|
|
71
|
+
|
|
72
|
+
vector_store = None
|
|
73
|
+
try:
|
|
74
|
+
from ..core.vector_store import VectorStore
|
|
75
|
+
|
|
76
|
+
vs = VectorStore(repo.mem_dir)
|
|
77
|
+
vs._get_connection() # ensure sqlite-vec is usable; may raise
|
|
78
|
+
vector_store = vs
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
access_index = AccessIndex(repo.mem_dir)
|
|
83
|
+
recall_engine = RecallEngine(
|
|
84
|
+
repo=repo,
|
|
85
|
+
vector_store=vector_store,
|
|
86
|
+
access_index=access_index,
|
|
87
|
+
use_cache=True,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
strategy = "hybrid" if args.strategy == "balanced" else args.strategy
|
|
91
|
+
pack_engine = PackEngine(
|
|
92
|
+
recall_engine=recall_engine,
|
|
93
|
+
model=args.model,
|
|
94
|
+
summarization_cascade=False,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
result = pack_engine.pack(
|
|
98
|
+
context=args.context,
|
|
99
|
+
budget=args.budget,
|
|
100
|
+
strategy=strategy,
|
|
101
|
+
exclude=args.exclude,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if args.format == "json":
|
|
105
|
+
import json
|
|
106
|
+
|
|
107
|
+
print(
|
|
108
|
+
json.dumps(
|
|
109
|
+
{
|
|
110
|
+
"content": result.content,
|
|
111
|
+
"total_tokens": result.total_tokens,
|
|
112
|
+
"budget": result.budget,
|
|
113
|
+
"items_used": result.items_used,
|
|
114
|
+
"items_total": result.items_total,
|
|
115
|
+
},
|
|
116
|
+
indent=2,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
print(result.content)
|
|
121
|
+
print(
|
|
122
|
+
f"\n# Pack stats: {result.total_tokens}/{result.budget} tokens, "
|
|
123
|
+
f"{result.items_used}/{result.items_total} items",
|
|
124
|
+
file=sys.stderr,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if vector_store and hasattr(vector_store, "close"):
|
|
128
|
+
vector_store.close()
|
|
129
|
+
return 0
|