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.
- package/LICENSE +21 -0
- package/README.md +729 -0
- package/agentram/__init__.py +26 -0
- package/agentram/adapter.py +368 -0
- package/agentram/cli.py +242 -0
- package/agentram/codex_wrapper.py +163 -0
- package/agentram/config.py +87 -0
- package/agentram/daemon.py +106 -0
- package/agentram/demo.py +29 -0
- package/agentram/mcp_server.py +369 -0
- package/agentram/merger.py +66 -0
- package/agentram/orchestrator.py +45 -0
- package/agentram/retriever.py +51 -0
- package/agentram/schema.py +258 -0
- package/agentram/storage.py +104 -0
- package/agentram/worker.py +40 -0
- package/agentram_daemon.py +4 -0
- package/agentram_mcp_server.py +4 -0
- package/bin/agentram-mcp.js +24 -0
- package/bin/agentram.js +24 -0
- package/codex_ram.py +4 -0
- package/package.json +49 -0
- package/pyproject.toml +38 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .adapter import HeuristicMemoryAdapter, OllamaMemoryAdapter
|
|
11
|
+
from .config import anthropic_from_env, default_ram_root, env_value, merged_env, openai_compatible_from_env
|
|
12
|
+
from .orchestrator import AgentRAM
|
|
13
|
+
from .retriever import MemoryRetriever
|
|
14
|
+
from .schema import Event, EventType
|
|
15
|
+
from .storage import JsonGoalStore, JsonMemoryStore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class WrapperResult:
|
|
20
|
+
prompt: str
|
|
21
|
+
stdout: str
|
|
22
|
+
stderr: str
|
|
23
|
+
returncode: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_injected_prompt(user_prompt: str, context_block: str) -> str:
|
|
27
|
+
if not context_block:
|
|
28
|
+
return user_prompt
|
|
29
|
+
return (
|
|
30
|
+
"Relevant AgentRAM notes:\n"
|
|
31
|
+
"Use only when relevant. Prefer current files over RAM if conflict.\n\n"
|
|
32
|
+
f"{context_block}\n\n"
|
|
33
|
+
"User request:\n"
|
|
34
|
+
f"{user_prompt}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_adapter(kind: str, env: dict[str, str]):
|
|
39
|
+
if kind == "heuristic":
|
|
40
|
+
return HeuristicMemoryAdapter()
|
|
41
|
+
if kind == "ollama":
|
|
42
|
+
return OllamaMemoryAdapter(
|
|
43
|
+
model=env_value(env, "OLLAMA_MODEL", default="qwen2.5:7b") or "qwen2.5:7b",
|
|
44
|
+
host=env_value(env, "OLLAMA_HOST", default="http://localhost:11434") or "http://localhost:11434",
|
|
45
|
+
timeout_seconds=float(env_value(env, "AGENTRAM_TIMEOUT", default="120") or "120"),
|
|
46
|
+
)
|
|
47
|
+
if kind == "openai-compatible":
|
|
48
|
+
return openai_compatible_from_env()
|
|
49
|
+
if kind == "anthropic":
|
|
50
|
+
return anthropic_from_env()
|
|
51
|
+
raise ValueError(f"unknown adapter: {kind}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def run_wrapper(
|
|
55
|
+
user_prompt: str,
|
|
56
|
+
*,
|
|
57
|
+
task_id: str = "codex",
|
|
58
|
+
ram_root: str | None = None,
|
|
59
|
+
adapter_kind: str = "heuristic",
|
|
60
|
+
command: list[str] | None = None,
|
|
61
|
+
prompt_as_arg: bool = False,
|
|
62
|
+
dry_run: bool = False,
|
|
63
|
+
) -> WrapperResult:
|
|
64
|
+
merged = merged_env()
|
|
65
|
+
resolved_ram_root = Path(ram_root) if ram_root else default_ram_root()
|
|
66
|
+
store = JsonMemoryStore(resolved_ram_root / "memory.json")
|
|
67
|
+
goal_store = JsonGoalStore(resolved_ram_root / "goal_state.json")
|
|
68
|
+
context_block = MemoryRetriever(store, goal_store).context_block(user_prompt, task_id=task_id, limit=10)
|
|
69
|
+
injected_prompt = build_injected_prompt(user_prompt, context_block)
|
|
70
|
+
|
|
71
|
+
adapter = create_adapter(adapter_kind, merged)
|
|
72
|
+
ram = AgentRAM(root=resolved_ram_root, adapter=adapter)
|
|
73
|
+
await ram.start()
|
|
74
|
+
ram.emit(Event(type=EventType.USER_MESSAGE, payload={"content": user_prompt}, task_id=task_id))
|
|
75
|
+
|
|
76
|
+
stdout = ""
|
|
77
|
+
stderr = ""
|
|
78
|
+
returncode = 0
|
|
79
|
+
if dry_run or not command:
|
|
80
|
+
stdout = injected_prompt
|
|
81
|
+
else:
|
|
82
|
+
run_command = command + ([injected_prompt] if prompt_as_arg else [])
|
|
83
|
+
completed = subprocess.run(
|
|
84
|
+
run_command,
|
|
85
|
+
input=None if prompt_as_arg else injected_prompt,
|
|
86
|
+
text=True,
|
|
87
|
+
encoding="utf-8",
|
|
88
|
+
errors="replace",
|
|
89
|
+
capture_output=True,
|
|
90
|
+
check=False,
|
|
91
|
+
)
|
|
92
|
+
stdout = completed.stdout or ""
|
|
93
|
+
stderr = completed.stderr or ""
|
|
94
|
+
returncode = completed.returncode
|
|
95
|
+
ram.emit(
|
|
96
|
+
Event(
|
|
97
|
+
type=EventType.TOOL_CALL,
|
|
98
|
+
payload={"command": command, "returncode": returncode},
|
|
99
|
+
task_id=task_id,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
output = stdout.strip() or stderr.strip()
|
|
103
|
+
if output:
|
|
104
|
+
ram.emit(
|
|
105
|
+
Event(
|
|
106
|
+
type=EventType.AGENT_MESSAGE,
|
|
107
|
+
payload={"content": output[:4000]},
|
|
108
|
+
task_id=task_id,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
await ram.stop()
|
|
113
|
+
return WrapperResult(prompt=injected_prompt, stdout=stdout, stderr=stderr, returncode=returncode)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
117
|
+
parser = argparse.ArgumentParser(description="Inject AgentRAM context into Codex or another CLI command.")
|
|
118
|
+
parser.add_argument("prompt", help="User prompt to send to Codex/command.")
|
|
119
|
+
parser.add_argument("--task-id", default="codex", help="AgentRAM task id.")
|
|
120
|
+
parser.add_argument("--ram-root", default=None, help="AgentRAM storage root. Defaults to .agentram in the current project directory.")
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
"--adapter",
|
|
123
|
+
choices=["heuristic", "ollama", "openai-compatible", "anthropic"],
|
|
124
|
+
default="heuristic",
|
|
125
|
+
help="Memory writer adapter.",
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument("--dry-run", action="store_true", help="Print injected prompt without running command.")
|
|
128
|
+
parser.add_argument("--prompt-as-arg", action="store_true", help="Append injected prompt as final command argument instead of stdin.")
|
|
129
|
+
return parser
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def split_command_args(argv: list[str] | None) -> tuple[list[str] | None, list[str]]:
|
|
133
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
134
|
+
if "--" not in args:
|
|
135
|
+
return args, []
|
|
136
|
+
separator_index = args.index("--")
|
|
137
|
+
return args[:separator_index], args[separator_index + 1:]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main(argv: list[str] | None = None) -> int:
|
|
141
|
+
parser = build_parser()
|
|
142
|
+
parser_args, command = split_command_args(argv)
|
|
143
|
+
args = parser.parse_args(parser_args)
|
|
144
|
+
result = asyncio.run(
|
|
145
|
+
run_wrapper(
|
|
146
|
+
args.prompt,
|
|
147
|
+
task_id=args.task_id,
|
|
148
|
+
ram_root=args.ram_root,
|
|
149
|
+
adapter_kind=args.adapter,
|
|
150
|
+
command=command or None,
|
|
151
|
+
prompt_as_arg=args.prompt_as_arg,
|
|
152
|
+
dry_run=args.dry_run,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
if result.stdout:
|
|
156
|
+
print(result.stdout, end="" if result.stdout.endswith("\n") else "\n")
|
|
157
|
+
if result.stderr:
|
|
158
|
+
print(result.stderr, file=sys.stderr, end="" if result.stderr.endswith("\n") else "\n")
|
|
159
|
+
return result.returncode
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .adapter import AnthropicMemoryAdapter, OpenAICompatibleMemoryAdapter
|
|
7
|
+
|
|
8
|
+
DEFAULT_AGENT_MODEL_ENV_NAMES = (
|
|
9
|
+
"CODEX_MODEL",
|
|
10
|
+
"CODEX_DEFAULT_MODEL",
|
|
11
|
+
"CLAUDE_MODEL",
|
|
12
|
+
"CURSOR_MODEL",
|
|
13
|
+
"AGENT_MODEL",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DEFAULT_RAM_DIR_NAME = ".agentram"
|
|
18
|
+
|
|
19
|
+
def default_ram_root(cwd: str | Path | None = None) -> Path:
|
|
20
|
+
base = Path.cwd() if cwd is None else Path(cwd)
|
|
21
|
+
return base / DEFAULT_RAM_DIR_NAME
|
|
22
|
+
|
|
23
|
+
def load_env_file(path: str | Path = ".env") -> dict[str, str]:
|
|
24
|
+
env_path = Path(path)
|
|
25
|
+
if not env_path.exists():
|
|
26
|
+
return {}
|
|
27
|
+
values: dict[str, str] = {}
|
|
28
|
+
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
29
|
+
line = line.strip()
|
|
30
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
31
|
+
continue
|
|
32
|
+
key, value = line.split("=", 1)
|
|
33
|
+
values[key.strip()] = value.strip().strip('"').strip("'")
|
|
34
|
+
return values
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def merged_env(path: str | Path = ".env") -> dict[str, str]:
|
|
38
|
+
return {**os.environ, **load_env_file(path)}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def env_value(env: dict[str, str], *names: str, default: str | None = None) -> str | None:
|
|
42
|
+
for name in names:
|
|
43
|
+
value = env.get(name)
|
|
44
|
+
if value:
|
|
45
|
+
return value
|
|
46
|
+
return default
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def openai_compatible_from_env(path: str | Path = ".env") -> OpenAICompatibleMemoryAdapter:
|
|
50
|
+
env = merged_env(path)
|
|
51
|
+
model = env_value(env, "AGENTRAM_MODEL", "Model", "MODEL", *DEFAULT_AGENT_MODEL_ENV_NAMES)
|
|
52
|
+
base_url = env_value(env, "OPENAI_BASE_URL", "BASE_URL")
|
|
53
|
+
api_key = env_value(env, "OPENAI_API_KEY", "API_KEY", "GROQ_API_KEY")
|
|
54
|
+
if not model:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"Missing model. Set AGENTRAM_MODEL, Model, MODEL, or current agent model env such as CODEX_MODEL in .env."
|
|
57
|
+
)
|
|
58
|
+
if not base_url:
|
|
59
|
+
raise ValueError("Missing base URL. Set OPENAI_BASE_URL or BASE_URL in .env.")
|
|
60
|
+
if not api_key:
|
|
61
|
+
raise ValueError("Missing API key. Set OPENAI_API_KEY, API_KEY, or GROQ_API_KEY in .env.")
|
|
62
|
+
return OpenAICompatibleMemoryAdapter(
|
|
63
|
+
model=model,
|
|
64
|
+
base_url=base_url,
|
|
65
|
+
api_key=api_key,
|
|
66
|
+
timeout_seconds=float(env_value(env, "AGENTRAM_TIMEOUT", default="120") or "120"),
|
|
67
|
+
use_response_format=(env_value(env, "AGENTRAM_RESPONSE_FORMAT", default="false") or "false").lower() == "true",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def anthropic_from_env(path: str | Path = ".env") -> AnthropicMemoryAdapter:
|
|
72
|
+
env = merged_env(path)
|
|
73
|
+
model = env_value(env, "ANTHROPIC_MODEL", "CLAUDE_MODEL", "AGENTRAM_MODEL", "Model", "MODEL")
|
|
74
|
+
api_key = env_value(env, "ANTHROPIC_API_KEY")
|
|
75
|
+
base_url = env_value(env, "ANTHROPIC_BASE_URL", default="https://api.anthropic.com/v1")
|
|
76
|
+
if not model:
|
|
77
|
+
raise ValueError("Missing Anthropic model. Set ANTHROPIC_MODEL, CLAUDE_MODEL, AGENTRAM_MODEL, Model, or MODEL in .env.")
|
|
78
|
+
if not api_key:
|
|
79
|
+
raise ValueError("Missing Anthropic API key. Set ANTHROPIC_API_KEY in .env.")
|
|
80
|
+
return AnthropicMemoryAdapter(
|
|
81
|
+
model=model,
|
|
82
|
+
base_url=base_url or "https://api.anthropic.com/v1",
|
|
83
|
+
api_key=api_key,
|
|
84
|
+
timeout_seconds=float(env_value(env, "AGENTRAM_TIMEOUT", default="120") or "120"),
|
|
85
|
+
max_tokens=int(env_value(env, "AGENTRAM_MAX_TOKENS", default="512") or "512"),
|
|
86
|
+
anthropic_version=env_value(env, "ANTHROPIC_VERSION", default="2023-06-01") or "2023-06-01",
|
|
87
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .adapter import HeuristicMemoryAdapter
|
|
11
|
+
from .config import default_ram_root
|
|
12
|
+
from .merger import MemoryMerger
|
|
13
|
+
from .schema import Event
|
|
14
|
+
from .storage import JsonMemoryStore, JsonlEventLog
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class DaemonState:
|
|
19
|
+
offset: int = 0
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> dict[str, int]:
|
|
22
|
+
return {"offset": self.offset}
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_dict(cls, data: dict[str, Any]) -> "DaemonState":
|
|
26
|
+
return cls(offset=int(data.get("offset", 0)))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AgentRAMDaemon:
|
|
30
|
+
def __init__(self, ram_root: str | Path | None = None) -> None:
|
|
31
|
+
self.ram_root = Path(ram_root) if ram_root else default_ram_root()
|
|
32
|
+
self.event_log = JsonlEventLog(self.ram_root / "events.jsonl")
|
|
33
|
+
self.memory_store = JsonMemoryStore(self.ram_root / "memory.json")
|
|
34
|
+
self.state_path = self.ram_root / "daemon_state.json"
|
|
35
|
+
self.adapter = HeuristicMemoryAdapter()
|
|
36
|
+
self.merger = MemoryMerger(self.memory_store)
|
|
37
|
+
|
|
38
|
+
def load_state(self) -> DaemonState:
|
|
39
|
+
if not self.state_path.exists():
|
|
40
|
+
return DaemonState()
|
|
41
|
+
return DaemonState.from_dict(json.loads(self.state_path.read_text(encoding="utf-8")))
|
|
42
|
+
|
|
43
|
+
def save_state(self, state: DaemonState) -> None:
|
|
44
|
+
self.state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
self.state_path.write_text(json.dumps(state.to_dict(), indent=2), encoding="utf-8")
|
|
46
|
+
|
|
47
|
+
def ingest_new_events(self, limit: int | None = None) -> dict[str, Any]:
|
|
48
|
+
state = self.load_state()
|
|
49
|
+
events = self.event_log.read_all()
|
|
50
|
+
new_events = events[state.offset:]
|
|
51
|
+
if limit is not None:
|
|
52
|
+
new_events = new_events[:limit]
|
|
53
|
+
patches = self.adapter.create_patches(new_events, self.memory_store.list_items())
|
|
54
|
+
memory_items = self.merger.apply(patches)
|
|
55
|
+
next_offset = state.offset + len(new_events)
|
|
56
|
+
self.save_state(DaemonState(offset=next_offset))
|
|
57
|
+
return {
|
|
58
|
+
"read_offset": state.offset,
|
|
59
|
+
"write_offset": next_offset,
|
|
60
|
+
"event_count": len(new_events),
|
|
61
|
+
"patch_count": len(patches),
|
|
62
|
+
"memory_count": len(memory_items),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
def replay_all(self) -> dict[str, Any]:
|
|
66
|
+
events = self.event_log.read_all()
|
|
67
|
+
patches = self.adapter.create_patches(events, [])
|
|
68
|
+
memory_items = self.merger.apply(patches)
|
|
69
|
+
self.save_state(DaemonState(offset=len(events)))
|
|
70
|
+
return {
|
|
71
|
+
"write_offset": len(events),
|
|
72
|
+
"event_count": len(events),
|
|
73
|
+
"patch_count": len(patches),
|
|
74
|
+
"memory_count": len(memory_items),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def run_forever(self, interval_seconds: float = 1.0, once: bool = False) -> None:
|
|
78
|
+
while True:
|
|
79
|
+
result = self.ingest_new_events()
|
|
80
|
+
print(json.dumps(result, ensure_ascii=False), flush=True)
|
|
81
|
+
if once:
|
|
82
|
+
return
|
|
83
|
+
time.sleep(interval_seconds)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
87
|
+
parser = argparse.ArgumentParser(description="AgentRAM background event ingestion daemon.")
|
|
88
|
+
parser.add_argument("--ram-root", default=None, help="AgentRAM storage root. Defaults to .agentram in the current project directory.")
|
|
89
|
+
parser.add_argument("--interval", type=float, default=1.0, help="Polling interval in seconds.")
|
|
90
|
+
parser.add_argument("--once", action="store_true", help="Ingest pending events once and exit.")
|
|
91
|
+
parser.add_argument("--replay-all", action="store_true", help="Replay all events and reset daemon offset.")
|
|
92
|
+
return parser
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main(argv: list[str] | None = None) -> int:
|
|
96
|
+
args = build_parser().parse_args(argv)
|
|
97
|
+
daemon = AgentRAMDaemon(ram_root=args.ram_root)
|
|
98
|
+
if args.replay_all:
|
|
99
|
+
print(json.dumps(daemon.replay_all(), ensure_ascii=False))
|
|
100
|
+
return 0
|
|
101
|
+
daemon.run_forever(interval_seconds=args.interval, once=args.once)
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
raise SystemExit(main())
|
package/agentram/demo.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .orchestrator import AgentRAM
|
|
7
|
+
from .retriever import MemoryRetriever
|
|
8
|
+
from .schema import Event, EventType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def async_main() -> None:
|
|
12
|
+
ram = AgentRAM(root=Path(".agentram_demo"))
|
|
13
|
+
await ram.start()
|
|
14
|
+
ram.emit(Event(type=EventType.USER_MESSAGE, payload={"content": "Build model-agnostic AgentRAM for Codex/Claude/Cursor."}))
|
|
15
|
+
ram.emit(Event(type=EventType.READ_FILE, payload={"path": "agentram/orchestrator.py"}))
|
|
16
|
+
ram.emit(Event(type=EventType.DECISION, payload={"content": "Use Event Log as source of truth and Memory Patch as model contract."}))
|
|
17
|
+
ram.emit(Event(type=EventType.WRITE_FILE, payload={"path": "agentram/schema.py", "summary": "added normalized memory schema"}))
|
|
18
|
+
await ram.stop()
|
|
19
|
+
|
|
20
|
+
retriever = MemoryRetriever(ram.memory_store)
|
|
21
|
+
print(retriever.context_block("AgentRAM schema orchestrator", limit=10))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
asyncio.run(async_main())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
main()
|