agentram 0.1.1 → 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.
@@ -0,0 +1,15 @@
1
+ ---
2
+ description: Build or run AgentRAM multi-model coding orchestration.
3
+ argument-hint: [task]
4
+ ---
5
+
6
+ Use AgentRAM multi-model orchestration for this task.
7
+
8
+ 1. Call `agentram_read_coding_models`.
9
+ 2. If no endpoints exist, ask user to bind a model with `agentram_bind_coding_model`.
10
+ 3. Call `agentram_build_coding_orchestration` with:
11
+ - `task`: `$ARGUMENTS`
12
+ - `files`: relevant files if known
13
+ - `modalities`: `["text"]` unless images/audio are included
14
+ 4. If user explicitly requests execution, call `agentram_run_coding_orchestration` with `execute: true`.
15
+ 5. Summarize per-model outputs and disagreements.
package/README.md CHANGED
@@ -352,6 +352,53 @@ Minimum usage pattern:
352
352
  3. Call `agentram_check_goal_drift` before switching direction.
353
353
  4. Call `agentram_retrieve` again near context pressure or resume.
354
354
 
355
+
356
+ ## Multi-Model Coding Orchestration
357
+
358
+ AgentRAM can bind multiple coding model endpoints and let Claude Code or MCP clients orchestrate them in parallel. This keeps the `free-code` style command registry, but adds project RAM, Goal Stack, and multi-agent model routing.
359
+
360
+ Bind models:
361
+
362
+ ```bash
363
+ agentram bind-model claude claude-3-5-sonnet-latest --role reviewer --api-key-env ANTHROPIC_API_KEY
364
+ agentram bind-model openai-compatible qwen2.5-coder --role coder --base-url http://localhost:8000/v1 --api-key-env OPENAI_API_KEY
365
+ ```
366
+
367
+ Build a dry-run orchestration plan:
368
+
369
+ ```bash
370
+ agentram orchestrate "review CLI orchestration" --file agentram/cli.py
371
+ ```
372
+
373
+ Run endpoints concurrently when keys and base URLs are configured:
374
+
375
+ ```bash
376
+ agentram orchestrate "review CLI orchestration" --file agentram/cli.py --execute
377
+ ```
378
+
379
+ Claude/MCP tools:
380
+
381
+ ```text
382
+ agentram_bind_coding_model
383
+ agentram_read_coding_models
384
+ agentram_build_coding_orchestration
385
+ agentram_run_coding_orchestration
386
+ ```
387
+
388
+ TUI slash commands:
389
+
390
+ ```text
391
+ /bind-model claude claude-3-5-sonnet-latest role=reviewer api_key_env=ANTHROPIC_API_KEY
392
+ /models
393
+ /orchestrate review CLI orchestration
394
+ ```
395
+
396
+ Storage:
397
+
398
+ ```text
399
+ .agentram/agent_profile.json
400
+ ```
401
+
355
402
  ## MCP Tools
356
403
 
357
404
  | Tool | Purpose |
@@ -364,6 +411,10 @@ Minimum usage pattern:
364
411
  | `agentram_read_goal_state` | Read mission, goals, current task, histories. |
365
412
  | `agentram_update_goal_state` | Update Goal Stack fields. |
366
413
  | `agentram_check_goal_drift` | Warn only when task appears off-goal. |
414
+ | `agentram_bind_coding_model` | Bind provider/model/role/modalities endpoint. |
415
+ | `agentram_read_coding_models` | Read bound coding model endpoints. |
416
+ | `agentram_build_coding_orchestration` | Build multi-agent prompts without model calls. |
417
+ | `agentram_run_coding_orchestration` | Dry-run or execute endpoints concurrently. |
367
418
 
368
419
  ### Init
369
420
 
@@ -1,6 +1,7 @@
1
1
  from .adapter import AnthropicMemoryAdapter, HeuristicMemoryAdapter, OllamaMemoryAdapter, OpenAICompatibleMemoryAdapter
2
2
  from .config import anthropic_from_env, load_env_file, merged_env, openai_compatible_from_env
3
3
  from .daemon import AgentRAMDaemon
4
+ from .orchestration import AgentModelEndpoint, CodingOrchestrationRequest, MultiModelCodingOrchestrator
4
5
  from .orchestrator import AgentRAM
5
6
  from .retriever import MemoryRetriever
6
7
  from .schema import Event, EventType, MemoryItem, MemoryPatch, MemoryScope, MemoryType
@@ -8,6 +9,8 @@ from .schema import Event, EventType, MemoryItem, MemoryPatch, MemoryScope, Memo
8
9
  __all__ = [
9
10
  "AgentRAM",
10
11
  "AgentRAMDaemon",
12
+ "AgentModelEndpoint",
13
+ "CodingOrchestrationRequest",
11
14
  "AnthropicMemoryAdapter",
12
15
  "anthropic_from_env",
13
16
  "Event",
@@ -20,6 +23,7 @@ __all__ = [
20
23
  "MemoryRetriever",
21
24
  "MemoryScope",
22
25
  "MemoryType",
26
+ "MultiModelCodingOrchestrator",
23
27
  "OllamaMemoryAdapter",
24
28
  "openai_compatible_from_env",
25
29
  "OpenAICompatibleMemoryAdapter",
package/agentram/cli.py CHANGED
@@ -30,6 +30,9 @@ SLASH_HELP = """Slash commands:
30
30
  /replay Replay events into memory
31
31
  /agents List subagent presets
32
32
  /agent <name> <task> Build Claude Code subagent prompt
33
+ /models List bound coding models
34
+ /bind-model provider model Bind model endpoint
35
+ /orchestrate <task> Build multi-model agent plan
33
36
  /claude-install [path] Install .claude commands and agents
34
37
  /note <text> Save decision note
35
38
  /clear Clear chat
@@ -48,6 +51,20 @@ def build_parser() -> argparse.ArgumentParser:
48
51
  subparsers.add_parser("status", help="Show RAM file and item counts.")
49
52
  subparsers.add_parser("goal", help="Show goal stack.")
50
53
  subparsers.add_parser("agents", help="List subagent presets.")
54
+ subparsers.add_parser("models", help="List bound coding model endpoints.")
55
+
56
+ bind_model = subparsers.add_parser("bind-model", help="Bind a coding model endpoint.")
57
+ bind_model.add_argument("provider", help="Provider name, e.g. claude, openai-compatible, groq.")
58
+ bind_model.add_argument("model", help="Model name.")
59
+ bind_model.add_argument("--role", default="coder", help="Agent role.")
60
+ bind_model.add_argument("--base-url", default="", help="Provider base URL.")
61
+ bind_model.add_argument("--api-key-env", default="", help="Environment variable containing API key.")
62
+ bind_model.add_argument("--modalities", default="text", help="Comma-separated modalities.")
63
+
64
+ orchestrate = subparsers.add_parser("orchestrate", help="Build or run multi-model coding orchestration.")
65
+ orchestrate.add_argument("task", nargs="?", default="", help="Coding task.")
66
+ orchestrate.add_argument("--file", action="append", default=[], help="File path to include.")
67
+ orchestrate.add_argument("--execute", action="store_true", help="Call model endpoints concurrently.")
51
68
 
52
69
  retrieve = subparsers.add_parser("retrieve", help="Retrieve memory context.")
53
70
  retrieve.add_argument("query", nargs="?", default="", help="Search query.")
@@ -111,6 +128,16 @@ def run_command(args: argparse.Namespace) -> int:
111
128
  if command == "agents":
112
129
  print(agents_text())
113
130
  return 0
131
+ if command == "models":
132
+ print(models_text(context))
133
+ return 0
134
+ if command == "bind-model":
135
+ print(bind_model_text(context, vars(args)))
136
+ return 0
137
+ if command == "orchestrate":
138
+ task = args.task or input("Task > ").strip()
139
+ print(orchestrate_text(context, task, files=args.file, execute=args.execute))
140
+ return 0
114
141
  if command == "claude-install":
115
142
  print(install_claude_templates_text(Path(args.target), force=args.force))
116
143
  return 0
@@ -182,6 +209,19 @@ def handle_slash_command(context: McpContext, text: str, messages: list[str] | N
182
209
  return replay_text(context, {})
183
210
  if command == "/agents":
184
211
  return agents_text()
212
+ if command == "/models":
213
+ return models_text(context)
214
+ if command == "/bind-model":
215
+ if len(rest) < 2:
216
+ return "Usage: /bind-model <provider> <model> [role=coder] [base_url=...] [api_key_env=...] [modalities=text,image]"
217
+ options = {"provider": rest[0], "model": rest[1]}
218
+ for item in rest[2:]:
219
+ if "=" in item:
220
+ key, value = item.split("=", 1)
221
+ options[key.replace("-", "_")] = value
222
+ return bind_model_text(context, options)
223
+ if command == "/orchestrate":
224
+ return orchestrate_text(context, raw_rest, execute=False)
185
225
  if command == "/agent":
186
226
  if not rest:
187
227
  return "Usage: /agent <memory|planner|reviewer|documenter> <task>"
@@ -298,6 +338,55 @@ def agent_prompt_text(context: McpContext, name: str, task: str) -> str:
298
338
 
299
339
 
300
340
 
341
+
342
+ def models_text(context: McpContext) -> str:
343
+ data = payload(call_tool(context, "agentram_read_coding_models", {}))
344
+ endpoints = data.get("endpoints", [])
345
+ if not endpoints:
346
+ return "No coding models bound. Use /bind-model claude claude-3-5-sonnet-latest role=reviewer api_key_env=ANTHROPIC_API_KEY"
347
+ lines = ["Bound coding models:"]
348
+ for endpoint in endpoints:
349
+ lines.append(f"- {endpoint.get('id')} provider={endpoint.get('provider')} model={endpoint.get('model')} role={endpoint.get('role')} modalities={','.join(endpoint.get('modalities', []))}")
350
+ return "\n".join(lines)
351
+
352
+
353
+ def bind_model_text(context: McpContext, options: dict[str, object]) -> str:
354
+ modalities_value = str(options.get("modalities", "text"))
355
+ arguments = {
356
+ "id": str(options.get("id", "")) or None,
357
+ "provider": str(options.get("provider", "openai-compatible")),
358
+ "model": str(options.get("model", "")),
359
+ "role": str(options.get("role", "coder")),
360
+ "base_url": str(options.get("base_url", "")),
361
+ "api_key_env": str(options.get("api_key_env", "")),
362
+ "modalities": [item.strip() for item in modalities_value.split(",") if item.strip()],
363
+ "enabled": bool(options.get("enabled", True)),
364
+ }
365
+ arguments = {key: value for key, value in arguments.items() if value is not None}
366
+ data = payload(call_tool(context, "agentram_bind_coding_model", arguments))
367
+ endpoint = data.get("endpoint", {})
368
+ return f"Bound model: {endpoint.get('id')} provider={endpoint.get('provider')} model={endpoint.get('model')} role={endpoint.get('role')}"
369
+
370
+
371
+ def orchestrate_text(context: McpContext, task: str, files: list[str] | None = None, execute: bool = False) -> str:
372
+ if not task.strip():
373
+ return "Usage: /orchestrate <coding task>"
374
+ tool = "agentram_run_coding_orchestration" if execute else "agentram_build_coding_orchestration"
375
+ data = payload(call_tool(context, tool, {"task": task, "files": files or [], "execute": execute}))
376
+ if not execute:
377
+ lines = [f"Orchestration plan: agents={data.get('agent_count', 0)}"]
378
+ for agent in data.get("agents", []):
379
+ endpoint = agent.get("endpoint", {})
380
+ lines.append(f"- {endpoint.get('id')} {endpoint.get('provider')}:{endpoint.get('model')} role={endpoint.get('role')}")
381
+ return "\n".join(lines)
382
+ lines = [f"Orchestration run: agents={data.get('agent_count', 0)}"]
383
+ for result in data.get("results", []):
384
+ endpoint = result.get("endpoint", {})
385
+ status = "ok" if result.get("ok") else "error"
386
+ output = result.get("output") or result.get("error", "")
387
+ lines.append(f"- {endpoint.get('id')} {status}: {str(output)[:300]}")
388
+ return "\n".join(lines)
389
+
301
390
  def claude_template_root() -> Path:
302
391
  return Path(__file__).resolve().parent.parent / ".claude"
303
392
 
@@ -413,8 +502,9 @@ def sidebar_lines(context: McpContext) -> list[str]:
413
502
  "",
414
503
  "CLAUDE",
415
504
  " /claude-install .",
416
- " /agents",
417
- " /agent reviewer <task>",
505
+ " /models",
506
+ " /bind-model claude <model>",
507
+ " /orchestrate <task>",
418
508
  ]
419
509
 
420
510
 
@@ -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.1",
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",
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.1"
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"