agentram 0.1.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.
@@ -0,0 +1,258 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ from enum import Enum
6
+ from typing import Any, Literal
7
+ from uuid import uuid4
8
+
9
+
10
+ def utc_now() -> str:
11
+ return datetime.now(timezone.utc).isoformat()
12
+
13
+
14
+ class EventType(str, Enum):
15
+ READ_FILE = "READ_FILE"
16
+ WRITE_FILE = "WRITE_FILE"
17
+ TOOL_CALL = "TOOL_CALL"
18
+ DECISION = "DECISION"
19
+ USER_MESSAGE = "USER_MESSAGE"
20
+ AGENT_MESSAGE = "AGENT_MESSAGE"
21
+
22
+
23
+ class MemoryType(str, Enum):
24
+ DECISION = "decision"
25
+ FACT = "fact"
26
+ TASK_STATE = "task_state"
27
+ FILE_SUMMARY = "file_summary"
28
+ OPEN_QUESTION = "open_question"
29
+
30
+
31
+ class MemoryScope(str, Enum):
32
+ SESSION = "session"
33
+ TASK = "task"
34
+ PROJECT = "project"
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Event:
39
+ type: EventType
40
+ payload: dict[str, Any]
41
+ task_id: str = "default"
42
+ id: str = field(default_factory=lambda: f"evt_{uuid4().hex}")
43
+ created_at: str = field(default_factory=utc_now)
44
+
45
+ def to_dict(self) -> dict[str, Any]:
46
+ return {
47
+ "id": self.id,
48
+ "task_id": self.task_id,
49
+ "type": self.type.value,
50
+ "payload": self.payload,
51
+ "created_at": self.created_at,
52
+ }
53
+
54
+ @classmethod
55
+ def from_dict(cls, data: dict[str, Any]) -> "Event":
56
+ return cls(
57
+ id=data["id"],
58
+ task_id=data.get("task_id", "default"),
59
+ type=EventType(data["type"]),
60
+ payload=dict(data.get("payload", {})),
61
+ created_at=data["created_at"],
62
+ )
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class MemoryItem:
67
+ type: MemoryType
68
+ content: str
69
+ source_events: list[str]
70
+ task_id: str = "default"
71
+ related_files: list[str] = field(default_factory=list)
72
+ confidence: float = 0.8
73
+ scope: MemoryScope = MemoryScope.TASK
74
+ id: str = field(default_factory=lambda: f"mem_{uuid4().hex}")
75
+ created_at: str = field(default_factory=utc_now)
76
+ updated_at: str = field(default_factory=utc_now)
77
+ expires_at: str | None = None
78
+
79
+ def to_dict(self) -> dict[str, Any]:
80
+ return {
81
+ "id": self.id,
82
+ "task_id": self.task_id,
83
+ "type": self.type.value,
84
+ "content": self.content,
85
+ "source_events": self.source_events,
86
+ "related_files": self.related_files,
87
+ "confidence": self.confidence,
88
+ "scope": self.scope.value,
89
+ "created_at": self.created_at,
90
+ "updated_at": self.updated_at,
91
+ "expires_at": self.expires_at,
92
+ }
93
+
94
+ @classmethod
95
+ def from_dict(cls, data: dict[str, Any]) -> "MemoryItem":
96
+ return cls(
97
+ id=data["id"],
98
+ task_id=data.get("task_id", "default"),
99
+ type=MemoryType(data["type"]),
100
+ content=data["content"],
101
+ source_events=list(data.get("source_events", [])),
102
+ related_files=list(data.get("related_files", [])),
103
+ confidence=float(data.get("confidence", 0.8)),
104
+ scope=MemoryScope(data.get("scope", "task")),
105
+ created_at=data["created_at"],
106
+ updated_at=data.get("updated_at", data["created_at"]),
107
+ expires_at=data.get("expires_at"),
108
+ )
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class MemoryPatch:
113
+ op: Literal["upsert", "delete"]
114
+ item: MemoryItem | None = None
115
+ target_id: str | None = None
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class GoalTask:
120
+ title: str
121
+ status: str = "pending"
122
+ id: str = field(default_factory=lambda: f"task_{uuid4().hex}")
123
+
124
+ def to_dict(self) -> dict[str, Any]:
125
+ return {"id": self.id, "title": self.title, "status": self.status}
126
+
127
+ @classmethod
128
+ def from_dict(cls, data: dict[str, Any]) -> "GoalTask":
129
+ return cls(id=str(data.get("id") or f"task_{uuid4().hex}"), title=str(data.get("title", "")), status=str(data.get("status", "pending")))
130
+
131
+ @dataclass(frozen=True)
132
+ class GoalNode:
133
+ title: str
134
+ status: str = "pending"
135
+ tasks: list[GoalTask] = field(default_factory=list)
136
+ id: str = field(default_factory=lambda: f"goal_{uuid4().hex}")
137
+
138
+ def to_dict(self) -> dict[str, Any]:
139
+ return {"id": self.id, "title": self.title, "status": self.status, "tasks": [task.to_dict() for task in self.tasks]}
140
+
141
+ @classmethod
142
+ def from_dict(cls, data: dict[str, Any]) -> "GoalNode":
143
+ return cls(
144
+ id=str(data.get("id") or f"goal_{uuid4().hex}"),
145
+ title=str(data.get("title", "")),
146
+ status=str(data.get("status", "pending")),
147
+ tasks=[GoalTask.from_dict(item) for item in data.get("tasks", []) if isinstance(item, dict)],
148
+ )
149
+
150
+ @dataclass(frozen=True)
151
+ class Milestone:
152
+ title: str
153
+ status: str = "pending"
154
+ goals: list[GoalNode] = field(default_factory=list)
155
+ id: str = field(default_factory=lambda: f"mile_{uuid4().hex}")
156
+
157
+ def to_dict(self) -> dict[str, Any]:
158
+ return {"id": self.id, "title": self.title, "status": self.status, "goals": [goal.to_dict() for goal in self.goals]}
159
+
160
+ @classmethod
161
+ def from_dict(cls, data: dict[str, Any]) -> "Milestone":
162
+ return cls(
163
+ id=str(data.get("id") or f"mile_{uuid4().hex}"),
164
+ title=str(data.get("title", "")),
165
+ status=str(data.get("status", "pending")),
166
+ goals=[GoalNode.from_dict(item) for item in data.get("goals", []) if isinstance(item, dict)],
167
+ )
168
+
169
+ @dataclass(frozen=True)
170
+ class DecisionRecord:
171
+ content: str
172
+ source_events: list[str] = field(default_factory=list)
173
+ confidence: float = 0.8
174
+ id: str = field(default_factory=lambda: f"dec_{uuid4().hex}")
175
+ created_at: str = field(default_factory=utc_now)
176
+
177
+ def to_dict(self) -> dict[str, Any]:
178
+ return {"id": self.id, "content": self.content, "source_events": self.source_events, "confidence": self.confidence, "created_at": self.created_at}
179
+
180
+ @classmethod
181
+ def from_dict(cls, data: dict[str, Any]) -> "DecisionRecord":
182
+ return cls(
183
+ id=str(data.get("id") or f"dec_{uuid4().hex}"),
184
+ content=str(data.get("content", "")),
185
+ source_events=list(data.get("source_events", [])),
186
+ confidence=float(data.get("confidence", 0.8)),
187
+ created_at=str(data.get("created_at") or utc_now()),
188
+ )
189
+
190
+ @dataclass(frozen=True)
191
+ class GoalState:
192
+ mission: str = ""
193
+ project_goal: str = ""
194
+ milestones: list[Milestone] = field(default_factory=list)
195
+ completed_milestones: list[str] = field(default_factory=list)
196
+ current_milestone: str = ""
197
+ current_goal: str = ""
198
+ current_task: str = ""
199
+ next_tasks: list[str] = field(default_factory=list)
200
+ goal_history: list[str] = field(default_factory=list)
201
+ decision_history: list[DecisionRecord] = field(default_factory=list)
202
+ updated_at: str = field(default_factory=utc_now)
203
+
204
+ def to_dict(self) -> dict[str, Any]:
205
+ return {
206
+ "mission": self.mission,
207
+ "project_goal": self.project_goal,
208
+ "milestones": [milestone.to_dict() for milestone in self.milestones],
209
+ "completed_milestones": self.completed_milestones,
210
+ "current_milestone": self.current_milestone,
211
+ "current_goal": self.current_goal,
212
+ "current_task": self.current_task,
213
+ "next_tasks": self.next_tasks,
214
+ "goal_history": self.goal_history,
215
+ "decision_history": [decision.to_dict() for decision in self.decision_history],
216
+ "updated_at": self.updated_at,
217
+ }
218
+
219
+ @classmethod
220
+ def from_dict(cls, data: dict[str, Any]) -> "GoalState":
221
+ return cls(
222
+ mission=str(data.get("mission", "")),
223
+ project_goal=str(data.get("project_goal", "")),
224
+ milestones=[Milestone.from_dict(item) for item in data.get("milestones", []) if isinstance(item, dict)],
225
+ completed_milestones=[str(item) for item in data.get("completed_milestones", [])],
226
+ current_milestone=str(data.get("current_milestone", "")),
227
+ current_goal=str(data.get("current_goal", "")),
228
+ current_task=str(data.get("current_task", "")),
229
+ next_tasks=[str(item) for item in data.get("next_tasks", [])],
230
+ goal_history=[str(item) for item in data.get("goal_history", [])],
231
+ decision_history=[DecisionRecord.from_dict(item) for item in data.get("decision_history", []) if isinstance(item, dict)],
232
+ updated_at=str(data.get("updated_at") or utc_now()),
233
+ )
234
+
235
+ def context_block(self) -> str:
236
+ lines: list[str] = []
237
+ if self.mission:
238
+ lines.append(f"Mission: {self.mission}")
239
+ if self.project_goal:
240
+ lines.append(f"Project Goal: {self.project_goal}")
241
+ if self.current_milestone:
242
+ lines.append(f"Current Milestone: {self.current_milestone}")
243
+ if self.current_goal:
244
+ lines.append(f"Current Goal: {self.current_goal}")
245
+ if self.current_task:
246
+ lines.append(f"Current Task: {self.current_task}")
247
+ if self.next_tasks:
248
+ lines.append("Next Tasks: " + "; ".join(self.next_tasks[:8]))
249
+ if self.completed_milestones:
250
+ lines.append("Completed Milestones: " + "; ".join(self.completed_milestones[:8]))
251
+ if self.goal_history:
252
+ lines.append("Goal History: " + " -> ".join(self.goal_history[-8:]))
253
+ if self.decision_history:
254
+ lines.append("Decision History:")
255
+ lines.extend(f"- {decision.content}" for decision in self.decision_history[-8:])
256
+ if not lines:
257
+ return ""
258
+ return "AgentRAM Goal Stack:\n" + "\n".join(lines)
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from .schema import Event, GoalState, MemoryItem, utc_now
7
+
8
+
9
+ class JsonlEventLog:
10
+ def __init__(self, path: str | Path) -> None:
11
+ self.path = Path(path)
12
+ self.path.parent.mkdir(parents=True, exist_ok=True)
13
+
14
+ def append(self, event: Event) -> None:
15
+ with self.path.open("a", encoding="utf-8") as file:
16
+ file.write(json.dumps(event.to_dict(), ensure_ascii=False) + "\n")
17
+
18
+ def read_all(self) -> list[Event]:
19
+ if not self.path.exists():
20
+ return []
21
+ events: list[Event] = []
22
+ with self.path.open("r", encoding="utf-8") as file:
23
+ for line in file:
24
+ if line.strip():
25
+ events.append(Event.from_dict(json.loads(line)))
26
+ return events
27
+
28
+
29
+ class JsonMemoryStore:
30
+ def __init__(self, path: str | Path) -> None:
31
+ self.path = Path(path)
32
+ self.path.parent.mkdir(parents=True, exist_ok=True)
33
+
34
+ def list_items(self) -> list[MemoryItem]:
35
+ if not self.path.exists():
36
+ return []
37
+ data = json.loads(self.path.read_text(encoding="utf-8"))
38
+ return [MemoryItem.from_dict(item) for item in data.get("items", [])]
39
+
40
+ def save_items(self, items: list[MemoryItem]) -> None:
41
+ payload = {
42
+ "updated_at": utc_now(),
43
+ "items": [item.to_dict() for item in items],
44
+ }
45
+ self.path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
46
+
47
+
48
+ class JsonGoalStore:
49
+ def __init__(self, path: str | Path) -> None:
50
+ self.path = Path(path)
51
+ self.path.parent.mkdir(parents=True, exist_ok=True)
52
+
53
+ def load(self) -> GoalState:
54
+ if not self.path.exists():
55
+ return GoalState()
56
+ data = json.loads(self.path.read_text(encoding="utf-8"))
57
+ return GoalState.from_dict(data)
58
+
59
+ def save(self, state: GoalState) -> GoalState:
60
+ payload = state.to_dict()
61
+ payload["updated_at"] = utc_now()
62
+ self.path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
63
+ return GoalState.from_dict(payload)
64
+
65
+ def update(self, patch: dict[str, object]) -> GoalState:
66
+ current = self.load().to_dict()
67
+ for key, value in patch.items():
68
+ if key == "decision_history" and isinstance(value, list):
69
+ current[key] = current.get(key, []) + value
70
+ elif key == "goal_history" and isinstance(value, list):
71
+ current[key] = current.get(key, []) + value
72
+ elif key in current:
73
+ current[key] = value
74
+ return self.save(GoalState.from_dict(current))
75
+
76
+
77
+ def ensure_ram_storage(root: str | Path) -> dict[str, object]:
78
+ ram_root = Path(root)
79
+ ram_root.mkdir(parents=True, exist_ok=True)
80
+
81
+ events_path = ram_root / "events.jsonl"
82
+ memory_path = ram_root / "memory.json"
83
+ goal_path = ram_root / "goal_state.json"
84
+
85
+ created: list[str] = []
86
+ if not events_path.exists():
87
+ events_path.write_text("", encoding="utf-8")
88
+ created.append("events.jsonl")
89
+ if not memory_path.exists():
90
+ JsonMemoryStore(memory_path).save_items([])
91
+ created.append("memory.json")
92
+ if not goal_path.exists():
93
+ JsonGoalStore(goal_path).save(GoalState())
94
+ created.append("goal_state.json")
95
+
96
+ return {
97
+ "ram_root": str(ram_root),
98
+ "created": created,
99
+ "files": {
100
+ "events": str(events_path),
101
+ "memory": str(memory_path),
102
+ "goal_state": str(goal_path),
103
+ },
104
+ }
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from .adapter import MemoryModelAdapter
6
+ from .merger import MemoryMerger
7
+ from .schema import Event
8
+
9
+
10
+ class MemoryWorker:
11
+ def __init__(
12
+ self,
13
+ queue: asyncio.Queue[Event],
14
+ adapter: MemoryModelAdapter,
15
+ merger: MemoryMerger,
16
+ batch_size: int = 8,
17
+ ) -> None:
18
+ self.queue = queue
19
+ self.adapter = adapter
20
+ self.merger = merger
21
+ self.batch_size = batch_size
22
+ self._running = False
23
+
24
+ async def run(self) -> None:
25
+ self._running = True
26
+ while self._running:
27
+ batch = [await self.queue.get()]
28
+ while len(batch) < self.batch_size:
29
+ try:
30
+ batch.append(self.queue.get_nowait())
31
+ except asyncio.QueueEmpty:
32
+ break
33
+ current = self.merger.store.list_items()
34
+ patches = self.adapter.create_patches(batch, current)
35
+ self.merger.apply(patches)
36
+ for _ in batch:
37
+ self.queue.task_done()
38
+
39
+ def stop(self) -> None:
40
+ self._running = False
@@ -0,0 +1,4 @@
1
+ from agentram.daemon import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,4 @@
1
+ from agentram.mcp_server import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ const { spawnSync } = require("node:child_process");
3
+ const path = require("node:path");
4
+
5
+ const packageRoot = path.resolve(__dirname, "..");
6
+ const existingPythonPath = process.env.PYTHONPATH || "";
7
+ const env = {
8
+ ...process.env,
9
+ PYTHONPATH: existingPythonPath ? `${packageRoot}${path.delimiter}${existingPythonPath}` : packageRoot,
10
+ };
11
+
12
+ function run(command, args) {
13
+ return spawnSync(command, args, { stdio: "inherit", env });
14
+ }
15
+
16
+ let result = run(process.env.PYTHON || "python", ["-m", "agentram.mcp_server", ...process.argv.slice(2)]);
17
+ if (result.error && process.platform === "win32") {
18
+ result = run("py", ["-3", "-m", "agentram.mcp_server", ...process.argv.slice(2)]);
19
+ }
20
+ if (result.error) {
21
+ console.error(`AgentRAM MCP failed to start Python: ${result.error.message}`);
22
+ process.exit(1);
23
+ }
24
+ process.exit(result.status ?? 0);
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ const { spawnSync } = require("node:child_process");
3
+ const path = require("node:path");
4
+
5
+ const packageRoot = path.resolve(__dirname, "..");
6
+ const existingPythonPath = process.env.PYTHONPATH || "";
7
+ const env = {
8
+ ...process.env,
9
+ PYTHONPATH: existingPythonPath ? `${packageRoot}${path.delimiter}${existingPythonPath}` : packageRoot,
10
+ };
11
+
12
+ function run(command, args) {
13
+ return spawnSync(command, args, { stdio: "inherit", env });
14
+ }
15
+
16
+ let result = run(process.env.PYTHON || "python", ["-m", "agentram.cli", ...process.argv.slice(2)]);
17
+ if (result.error && process.platform === "win32") {
18
+ result = run("py", ["-3", "-m", "agentram.cli", ...process.argv.slice(2)]);
19
+ }
20
+ if (result.error) {
21
+ console.error(`AgentRAM failed to start Python: ${result.error.message}`);
22
+ process.exit(1);
23
+ }
24
+ process.exit(result.status ?? 0);
package/codex_ram.py ADDED
@@ -0,0 +1,4 @@
1
+ from agentram.codex_wrapper import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "agentram",
3
+ "version": "0.1.0",
4
+ "description": "Async, model-agnostic Working RAM for coding agents.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/trancongnghia/AGENT_RAM#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/trancongnghia/AGENT_RAM.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/trancongnghia/AGENT_RAM/issues"
13
+ },
14
+ "keywords": [
15
+ "agent",
16
+ "memory",
17
+ "codex",
18
+ "claude",
19
+ "mcp",
20
+ "llm"
21
+ ],
22
+ "bin": {
23
+ "agentram": "bin/agentram.js",
24
+ "agentram-mcp": "bin/agentram-mcp.js"
25
+ },
26
+ "scripts": {
27
+ "agentram": "node bin/agentram.js",
28
+ "agentram:mcp": "node bin/agentram-mcp.js",
29
+ "test": "python -m pytest -q",
30
+ "pack:dry-run": "npm pack --dry-run",
31
+ "prepublishOnly": "npm test"
32
+ },
33
+ "files": [
34
+ "agentram/*.py",
35
+ "bin/*.js",
36
+ "agentram_mcp_server.py",
37
+ "agentram_daemon.py",
38
+ "codex_ram.py",
39
+ "pyproject.toml",
40
+ "README.md",
41
+ "LICENSE"
42
+ ],
43
+ "engines": {
44
+ "node": ">=16"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }
package/pyproject.toml ADDED
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "agentram"
7
+ version = "0.1.0"
8
+ description = "Async, model-agnostic RAM layer for agentic coding tools."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [{name = "AgentRAM contributors"}]
12
+ keywords = ["agent", "memory", "codex", "mcp", "llm"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Topic :: Software Development :: Libraries",
19
+ ]
20
+
21
+ requires-python = ">=3.10"
22
+ dependencies = []
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/your-org/agentram"
26
+ Repository = "https://github.com/your-org/agentram"
27
+ Issues = "https://github.com/your-org/agentram/issues"
28
+
29
+ [project.scripts]
30
+ agentram = "agentram.cli:main"
31
+ agentram-demo = "agentram.demo:main"
32
+ agentram-codex = "agentram.codex_wrapper:main"
33
+ agentram-mcp = "agentram.mcp_server:main"
34
+ agentram-daemon = "agentram.daemon:main"
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
38
+ pythonpath = ["."]