agentram 0.1.0 → 0.1.2

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.
@@ -11,9 +11,10 @@ from typing import Any
11
11
  from .adapter import HeuristicMemoryAdapter
12
12
  from .config import default_ram_root
13
13
  from .merger import MemoryMerger
14
+ from .orchestration import AgentModelEndpoint, CodingOrchestrationRequest, MultiModelCodingOrchestrator
14
15
  from .retriever import MemoryRetriever
15
16
  from .schema import Event, EventType
16
- from .storage import JsonGoalStore, JsonMemoryStore, JsonlEventLog, ensure_ram_storage
17
+ from .storage import JsonAgentProfileStore, JsonGoalStore, JsonMemoryStore, JsonlEventLog, ensure_ram_storage
17
18
 
18
19
  JSONRPC_VERSION = "2.0"
19
20
  SERVER_NAME = "agentram"
@@ -43,6 +44,10 @@ class McpContext:
43
44
  def goal_store(self) -> JsonGoalStore:
44
45
  return JsonGoalStore(self.ram_root / "goal_state.json")
45
46
 
47
+ @property
48
+ def profile_store(self) -> JsonAgentProfileStore:
49
+ return JsonAgentProfileStore(self.ram_root / "agent_profile.json")
50
+
46
51
  @property
47
52
  def merger(self) -> MemoryMerger:
48
53
  return MemoryMerger(self.memory_store)
@@ -220,6 +225,60 @@ def tools_list() -> dict[str, Any]:
220
225
  },
221
226
  },
222
227
  },
228
+
229
+ {
230
+ "name": "agentram_bind_coding_model",
231
+ "description": "Bind a coding-agent model endpoint that Claude or other agents can orchestrate later.",
232
+ "inputSchema": {
233
+ "type": "object",
234
+ "properties": {
235
+ "id": {"type": "string"},
236
+ "provider": {"type": "string", "default": "openai-compatible"},
237
+ "model": {"type": "string"},
238
+ "role": {"type": "string", "default": "coder"},
239
+ "base_url": {"type": "string"},
240
+ "api_key_env": {"type": "string"},
241
+ "modalities": {"type": "array", "items": {"type": "string"}},
242
+ "enabled": {"type": "boolean", "default": True},
243
+ },
244
+ "required": ["provider", "model"],
245
+ },
246
+ },
247
+ {
248
+ "name": "agentram_read_coding_models",
249
+ "description": "Read coding-agent model endpoints bound to this project RAM.",
250
+ "inputSchema": {"type": "object", "properties": {}},
251
+ },
252
+ {
253
+ "name": "agentram_build_coding_orchestration",
254
+ "description": "Build prompts for all enabled coding model endpoints without calling models.",
255
+ "inputSchema": {
256
+ "type": "object",
257
+ "properties": {
258
+ "task": {"type": "string"},
259
+ "query": {"type": "string"},
260
+ "files": {"type": "array", "items": {"type": "string"}},
261
+ "modalities": {"type": "array", "items": {"type": "string"}},
262
+ },
263
+ "required": ["task"],
264
+ },
265
+ },
266
+ {
267
+ "name": "agentram_run_coding_orchestration",
268
+ "description": "Run enabled coding model endpoints concurrently when execute=true, otherwise return dry-run plan.",
269
+ "inputSchema": {
270
+ "type": "object",
271
+ "properties": {
272
+ "task": {"type": "string"},
273
+ "query": {"type": "string"},
274
+ "files": {"type": "array", "items": {"type": "string"}},
275
+ "modalities": {"type": "array", "items": {"type": "string"}},
276
+ "execute": {"type": "boolean", "default": False},
277
+ "max_workers": {"type": "integer", "default": 4},
278
+ },
279
+ "required": ["task"],
280
+ },
281
+ },
223
282
  {
224
283
  "name": "agentram_check_goal_drift",
225
284
  "description": "Check whether a proposed task aligns with the current Goal Stack. Returns no suggestions when aligned.",
@@ -284,6 +343,32 @@ def call_tool(context: McpContext, name: str, arguments: dict[str, Any]) -> dict
284
343
  state = context.goal_store.update(arguments)
285
344
  return tool_result({"ok": True, "goal_state": state.to_dict(), "context": state.context_block()})
286
345
 
346
+
347
+ if name == "agentram_bind_coding_model":
348
+ endpoint = AgentModelEndpoint.from_dict(arguments)
349
+ data = context.profile_store.upsert(endpoint)
350
+ return tool_result({"ok": True, "endpoint": endpoint.to_dict(), "profile": data})
351
+
352
+ if name == "agentram_read_coding_models":
353
+ endpoints = context.profile_store.list_endpoints()
354
+ return tool_result({"endpoints": [endpoint.to_dict() for endpoint in endpoints]})
355
+
356
+ if name == "agentram_build_coding_orchestration":
357
+ request = orchestration_request_from_args(context, arguments)
358
+ endpoints = context.profile_store.list_endpoints()
359
+ plan = MultiModelCodingOrchestrator(endpoints).build_plan(request)
360
+ return tool_result({"ok": True, "execute": False, **plan})
361
+
362
+ if name == "agentram_run_coding_orchestration":
363
+ request = orchestration_request_from_args(context, arguments)
364
+ endpoints = context.profile_store.list_endpoints()
365
+ orchestrator = MultiModelCodingOrchestrator(endpoints)
366
+ if not bool(arguments.get("execute", False)):
367
+ plan = orchestrator.build_plan(request)
368
+ return tool_result({"ok": True, "execute": False, **plan})
369
+ result = orchestrator.run(request, max_workers=int(arguments.get("max_workers", 4)))
370
+ return tool_result({"ok": True, "execute": True, **result})
371
+
287
372
  if name == "agentram_check_goal_drift":
288
373
  task = str(arguments.get("task", ""))
289
374
  if not task.strip():
@@ -293,6 +378,27 @@ def call_tool(context: McpContext, name: str, arguments: dict[str, Any]) -> dict
293
378
  raise ValueError(f"unknown tool: {name}")
294
379
 
295
380
 
381
+
382
+ def orchestration_request_from_args(context: McpContext, arguments: dict[str, Any]) -> CodingOrchestrationRequest:
383
+ task = str(arguments.get("task", ""))
384
+ if not task.strip():
385
+ raise ValueError("task is required")
386
+ query = str(arguments.get("query") or task)
387
+ files = arguments.get("files", [])
388
+ modalities = arguments.get("modalities", ["text"])
389
+ if not isinstance(files, list):
390
+ raise ValueError("files must be an array")
391
+ if not isinstance(modalities, list):
392
+ raise ValueError("modalities must be an array")
393
+ retriever = MemoryRetriever(context.memory_store, context.goal_store)
394
+ context_block = retriever.context_block(query=query, task_id="codex", limit=8)
395
+ return CodingOrchestrationRequest(
396
+ task=task,
397
+ context=context_block,
398
+ files=[str(item) for item in files],
399
+ modalities=[str(item) for item in modalities],
400
+ )
401
+
296
402
  def handle_request(context: McpContext, request: dict[str, Any]) -> dict[str, Any] | None:
297
403
  request_id = request.get("id")
298
404
  method = request.get("method")
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ from concurrent.futures import ThreadPoolExecutor, as_completed
4
+ from dataclasses import dataclass, field
5
+ import json
6
+ import os
7
+ import urllib.request
8
+ from typing import Any, Callable
9
+ from uuid import uuid4
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class AgentModelEndpoint:
14
+ provider: str
15
+ model: str
16
+ role: str = "coder"
17
+ base_url: str = ""
18
+ api_key_env: str = ""
19
+ modalities: list[str] = field(default_factory=lambda: ["text"])
20
+ enabled: bool = True
21
+ id: str = field(default_factory=lambda: f"model_{uuid4().hex[:10]}")
22
+
23
+ def to_dict(self) -> dict[str, Any]:
24
+ return {
25
+ "id": self.id,
26
+ "provider": self.provider,
27
+ "model": self.model,
28
+ "role": self.role,
29
+ "base_url": self.base_url,
30
+ "api_key_env": self.api_key_env,
31
+ "modalities": self.modalities,
32
+ "enabled": self.enabled,
33
+ }
34
+
35
+ @classmethod
36
+ def from_dict(cls, data: dict[str, Any]) -> "AgentModelEndpoint":
37
+ return cls(
38
+ id=str(data.get("id") or f"model_{uuid4().hex[:10]}"),
39
+ provider=str(data.get("provider", "openai-compatible")),
40
+ model=str(data.get("model", "")),
41
+ role=str(data.get("role", "coder")),
42
+ base_url=str(data.get("base_url", "")),
43
+ api_key_env=str(data.get("api_key_env", "")),
44
+ modalities=[str(item) for item in data.get("modalities", ["text"])],
45
+ enabled=bool(data.get("enabled", True)),
46
+ )
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class CodingOrchestrationRequest:
51
+ task: str
52
+ context: str = ""
53
+ files: list[str] = field(default_factory=list)
54
+ modalities: list[str] = field(default_factory=lambda: ["text"])
55
+
56
+ def to_prompt(self, endpoint: AgentModelEndpoint) -> str:
57
+ return "\n".join(
58
+ [
59
+ f"You are AgentRAM {endpoint.role} agent.",
60
+ f"Model: {endpoint.model}",
61
+ f"Provider: {endpoint.provider}",
62
+ f"Modalities: {', '.join(endpoint.modalities)}",
63
+ "Task:",
64
+ self.task,
65
+ "Files:",
66
+ "\n".join(f"- {item}" for item in self.files) or "- none provided",
67
+ "AgentRAM Context:",
68
+ self.context or "No context provided.",
69
+ "Output contract:",
70
+ "- Return concise coding findings or patch plan.",
71
+ "- Mention files to inspect or change.",
72
+ "- Do not invent repo facts.",
73
+ ]
74
+ )
75
+
76
+
77
+ class MultiModelCodingOrchestrator:
78
+ def __init__(self, endpoints: list[AgentModelEndpoint], client: Callable[[AgentModelEndpoint, str], str] | None = None) -> None:
79
+ self.endpoints = [endpoint for endpoint in endpoints if endpoint.enabled]
80
+ self.client = client or self._call_endpoint
81
+
82
+ def build_plan(self, request: CodingOrchestrationRequest) -> dict[str, Any]:
83
+ agents = []
84
+ for endpoint in self.endpoints:
85
+ agents.append({"endpoint": endpoint.to_dict(), "prompt": request.to_prompt(endpoint)})
86
+ return {"task": request.task, "agent_count": len(agents), "agents": agents}
87
+
88
+ def run(self, request: CodingOrchestrationRequest, max_workers: int = 4) -> dict[str, Any]:
89
+ results: list[dict[str, Any]] = []
90
+ if not self.endpoints:
91
+ return {"task": request.task, "agent_count": 0, "results": []}
92
+ with ThreadPoolExecutor(max_workers=max(1, min(max_workers, len(self.endpoints)))) as executor:
93
+ future_map = {executor.submit(self.client, endpoint, request.to_prompt(endpoint)): endpoint for endpoint in self.endpoints}
94
+ for future in as_completed(future_map):
95
+ endpoint = future_map[future]
96
+ try:
97
+ output = future.result()
98
+ results.append({"endpoint": endpoint.to_dict(), "ok": True, "output": output})
99
+ except Exception as error: # noqa: BLE001 - agent boundary
100
+ results.append({"endpoint": endpoint.to_dict(), "ok": False, "error": str(error)})
101
+ return {"task": request.task, "agent_count": len(self.endpoints), "results": results}
102
+
103
+ def _call_endpoint(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
104
+ provider = endpoint.provider.lower()
105
+ if provider in {"openai", "openai-compatible", "groq", "vllm", "lmstudio", "ollama-v1"}:
106
+ return self._call_openai_compatible(endpoint, prompt)
107
+ if provider in {"anthropic", "claude"}:
108
+ return self._call_anthropic(endpoint, prompt)
109
+ raise ValueError(f"Unsupported provider for execution: {endpoint.provider}")
110
+
111
+ def _call_openai_compatible(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
112
+ api_key = os.getenv(endpoint.api_key_env) if endpoint.api_key_env else ""
113
+ if not endpoint.base_url:
114
+ raise ValueError("base_url is required")
115
+ if endpoint.api_key_env and not api_key:
116
+ raise ValueError(f"missing API key env: {endpoint.api_key_env}")
117
+ payload = {"model": endpoint.model, "messages": [{"role": "user", "content": prompt}], "temperature": 0}
118
+ request = urllib.request.Request(
119
+ endpoint.base_url.rstrip("/") + "/chat/completions",
120
+ data=json.dumps(payload).encode("utf-8"),
121
+ headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"} if api_key else {"Content-Type": "application/json"},
122
+ method="POST",
123
+ )
124
+ with urllib.request.urlopen(request, timeout=120) as response:
125
+ data = json.loads(response.read().decode("utf-8"))
126
+ return str(data.get("choices", [{}])[0].get("message", {}).get("content", ""))
127
+
128
+ def _call_anthropic(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
129
+ api_key = os.getenv(endpoint.api_key_env) if endpoint.api_key_env else ""
130
+ if endpoint.api_key_env and not api_key:
131
+ raise ValueError(f"missing API key env: {endpoint.api_key_env}")
132
+ base_url = endpoint.base_url.rstrip("/") if endpoint.base_url else "https://api.anthropic.com/v1"
133
+ payload = {"model": endpoint.model, "max_tokens": 1024, "messages": [{"role": "user", "content": prompt}]}
134
+ request = urllib.request.Request(
135
+ base_url + "/messages",
136
+ data=json.dumps(payload).encode("utf-8"),
137
+ headers={"Content-Type": "application/json", "x-api-key": api_key, "anthropic-version": "2023-06-01"},
138
+ method="POST",
139
+ )
140
+ with urllib.request.urlopen(request, timeout=120) as response:
141
+ data = json.loads(response.read().decode("utf-8"))
142
+ content = data.get("content", [])
143
+ if content and isinstance(content, list):
144
+ return "\n".join(str(item.get("text", "")) for item in content if isinstance(item, dict))
145
+ return str(data)
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  from pathlib import Path
5
5
 
6
+ from .orchestration import AgentModelEndpoint
6
7
  from .schema import Event, GoalState, MemoryItem, utc_now
7
8
 
8
9
 
@@ -74,6 +75,40 @@ class JsonGoalStore:
74
75
  return self.save(GoalState.from_dict(current))
75
76
 
76
77
 
78
+ class JsonAgentProfileStore:
79
+ def __init__(self, path: str | Path) -> None:
80
+ self.path = Path(path)
81
+ self.path.parent.mkdir(parents=True, exist_ok=True)
82
+
83
+ def load(self) -> dict[str, object]:
84
+ if not self.path.exists():
85
+ return {"updated_at": utc_now(), "endpoints": []}
86
+ return json.loads(self.path.read_text(encoding="utf-8"))
87
+
88
+ def save(self, endpoints: list[AgentModelEndpoint]) -> dict[str, object]:
89
+ payload = {"updated_at": utc_now(), "endpoints": [endpoint.to_dict() for endpoint in endpoints]}
90
+ self.path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
91
+ return payload
92
+
93
+ def list_endpoints(self) -> list[AgentModelEndpoint]:
94
+ data = self.load()
95
+ return [AgentModelEndpoint.from_dict(item) for item in data.get("endpoints", []) if isinstance(item, dict)]
96
+
97
+ def upsert(self, endpoint: AgentModelEndpoint) -> dict[str, object]:
98
+ endpoints = self.list_endpoints()
99
+ replaced = False
100
+ updated: list[AgentModelEndpoint] = []
101
+ for current in endpoints:
102
+ if current.id == endpoint.id:
103
+ updated.append(endpoint)
104
+ replaced = True
105
+ else:
106
+ updated.append(current)
107
+ if not replaced:
108
+ updated.append(endpoint)
109
+ return self.save(updated)
110
+
111
+
77
112
  def ensure_ram_storage(root: str | Path) -> dict[str, object]:
78
113
  ram_root = Path(root)
79
114
  ram_root.mkdir(parents=True, exist_ok=True)
@@ -81,6 +116,7 @@ def ensure_ram_storage(root: str | Path) -> dict[str, object]:
81
116
  events_path = ram_root / "events.jsonl"
82
117
  memory_path = ram_root / "memory.json"
83
118
  goal_path = ram_root / "goal_state.json"
119
+ profile_path = ram_root / "agent_profile.json"
84
120
 
85
121
  created: list[str] = []
86
122
  if not events_path.exists():
@@ -92,6 +128,9 @@ def ensure_ram_storage(root: str | Path) -> dict[str, object]:
92
128
  if not goal_path.exists():
93
129
  JsonGoalStore(goal_path).save(GoalState())
94
130
  created.append("goal_state.json")
131
+ if not profile_path.exists():
132
+ JsonAgentProfileStore(profile_path).save([])
133
+ created.append("agent_profile.json")
95
134
 
96
135
  return {
97
136
  "ram_root": str(ram_root),
@@ -100,5 +139,6 @@ def ensure_ram_storage(root: str | Path) -> dict[str, object]:
100
139
  "events": str(events_path),
101
140
  "memory": str(memory_path),
102
141
  "goal_state": str(goal_path),
142
+ "agent_profile": str(profile_path),
103
143
  },
104
144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentram",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Async, model-agnostic Working RAM for coding agents.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/trancongnghia/AGENT_RAM#readme",
@@ -33,6 +33,8 @@
33
33
  "files": [
34
34
  "agentram/*.py",
35
35
  "bin/*.js",
36
+ ".claude/agents/*.md",
37
+ ".claude/commands/*.md",
36
38
  "agentram_mcp_server.py",
37
39
  "agentram_daemon.py",
38
40
  "codex_ram.py",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentram"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Async, model-agnostic RAM layer for agentic coding tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"