agentram 0.1.41 → 0.1.43

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/agentram/cli.py CHANGED
@@ -13,7 +13,7 @@ import time
13
13
  from pathlib import Path
14
14
  from typing import Any
15
15
 
16
- from .config import default_ram_root
16
+ from .config import default_ram_root, global_agent_profile_enabled
17
17
  from .mcp_server import McpContext, call_tool
18
18
  from .orchestration import AgentModelEndpoint, MultiModelCodingOrchestrator
19
19
 
@@ -1250,6 +1250,8 @@ def bind_model_text(context: McpContext, options: dict[str, object]) -> str:
1250
1250
 
1251
1251
 
1252
1252
  def set_global_agent_if_available(context: McpContext, slot: str, endpoint: AgentModelEndpoint) -> bool:
1253
+ if not global_agent_profile_enabled():
1254
+ return False
1253
1255
  try:
1254
1256
  context.global_profile_store.set_agent(slot, endpoint)
1255
1257
  return True
@@ -29,6 +29,10 @@ def global_agent_profile_path() -> Path:
29
29
  return Path(base) / "AgentRAM" / "agent_profile.jsonl"
30
30
  return Path.home() / ".agentram" / "agent_profile.jsonl"
31
31
 
32
+ def global_agent_profile_enabled() -> bool:
33
+ value = os.getenv("AGENTRAM_DISABLE_GLOBAL_PROFILE", "").strip().lower()
34
+ return value not in {"1", "true", "yes", "on"}
35
+
32
36
  def load_env_file(path: str | Path = ".env") -> dict[str, str]:
33
37
  env_path = Path(path)
34
38
  if not env_path.exists():
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  from typing import Any
10
10
 
11
11
  from .adapter import HeuristicMemoryAdapter
12
- from .config import default_ram_root, global_agent_profile_path
12
+ from .config import default_ram_root, global_agent_profile_enabled, global_agent_profile_path
13
13
  from .merger import MemoryMerger
14
14
  from .orchestration import AgentModelEndpoint, CodingOrchestrationRequest, MultiModelCodingOrchestrator
15
15
  from .retriever import MemoryRetriever
@@ -46,7 +46,8 @@ class McpContext:
46
46
 
47
47
  @property
48
48
  def profile_store(self) -> JsonAgentProfileStore:
49
- return JsonAgentProfileStore(self.ram_root / "agent_profile.jsonl", fallback_path=global_agent_profile_path())
49
+ fallback = global_agent_profile_path() if global_agent_profile_enabled() else None
50
+ return JsonAgentProfileStore(self.ram_root / "agent_profile.jsonl", fallback_path=fallback)
50
51
 
51
52
  @property
52
53
  def global_profile_store(self) -> JsonAgentProfileStore:
@@ -3,12 +3,12 @@
3
3
  from concurrent.futures import ThreadPoolExecutor, as_completed
4
4
  from dataclasses import dataclass, field
5
5
  import json
6
- import os
7
- from pathlib import Path
8
- import re
9
- import shlex
10
- import subprocess
11
- import tempfile
6
+ import os
7
+ from pathlib import Path
8
+ import re
9
+ import shlex
10
+ import subprocess
11
+ import tempfile
12
12
  import urllib.error
13
13
  import urllib.request
14
14
  from typing import Any, Callable
@@ -46,7 +46,7 @@ def normalize_provider_name(provider: str) -> str:
46
46
  return PROVIDER_ALIASES.get(normalized, normalized)
47
47
 
48
48
 
49
- def split_cli_command(command: str) -> list[str]:
49
+ def split_cli_command(command: str) -> list[str]:
50
50
  cleaned = command.strip().strip('"')
51
51
  if not cleaned:
52
52
  return []
@@ -59,23 +59,23 @@ def split_cli_command(command: str) -> list[str]:
59
59
  program = cleaned[:end]
60
60
  rest = cleaned[end:].strip()
61
61
  return [program, *shlex.split(rest)] if rest else [program]
62
- return shlex.split(cleaned)
63
-
64
- def env_value(name: str) -> str:
65
- value = os.getenv(name, "")
66
- if value:
67
- return value
68
- env_path = Path(".env")
69
- if not env_path.exists():
70
- return ""
71
- for line in env_path.read_text(encoding="utf-8", errors="ignore").splitlines():
72
- stripped = line.strip()
73
- if not stripped or stripped.startswith("#") or "=" not in stripped:
74
- continue
75
- key, raw_value = stripped.split("=", 1)
76
- if key.strip() == name:
77
- return raw_value.strip().strip('"').strip("'")
78
- return ""
62
+ return shlex.split(cleaned)
63
+
64
+ def env_value(name: str) -> str:
65
+ value = os.getenv(name, "")
66
+ if value:
67
+ return value
68
+ env_path = Path(".env")
69
+ if not env_path.exists():
70
+ return ""
71
+ for line in env_path.read_text(encoding="utf-8", errors="ignore").splitlines():
72
+ stripped = line.strip()
73
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
74
+ continue
75
+ key, raw_value = stripped.split("=", 1)
76
+ if key.strip() == name:
77
+ return raw_value.strip().strip('"').strip("'")
78
+ return ""
79
79
 
80
80
 
81
81
  @dataclass(frozen=True)
@@ -182,7 +182,7 @@ class MultiModelCodingOrchestrator:
182
182
  raise ValueError(f"Unsupported provider for execution: {endpoint.provider}")
183
183
 
184
184
  def _call_openai_compatible(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
185
- api_key = env_value(endpoint.api_key_env) if endpoint.api_key_env else ""
185
+ api_key = env_value(endpoint.api_key_env) if endpoint.api_key_env else ""
186
186
  if not endpoint.base_url:
187
187
  raise ValueError("base_url is required")
188
188
  if endpoint.api_key_env and not api_key:
@@ -204,20 +204,20 @@ class MultiModelCodingOrchestrator:
204
204
  if error.code == 404:
205
205
  hint += " | base_url should include /v1 but not /chat/completions; verify provider URL and model name"
206
206
  raise RuntimeError(f"{hint} | response={body[:500]}") from error
207
- try:
208
- data = json.loads(raw)
209
- except json.JSONDecodeError as error:
210
- mixed_json = parse_openai_mixed_json(raw)
211
- if mixed_json:
212
- return extract_openai_message(mixed_json)
213
- streamed = parse_openai_sse(raw)
214
- if streamed:
215
- return streamed
216
- raise RuntimeError(f"OpenAI-compatible returned non-JSON at {url} | response={raw[:500]}") from error
217
- return extract_openai_message(data)
207
+ try:
208
+ data = json.loads(raw)
209
+ except json.JSONDecodeError as error:
210
+ mixed_json = parse_openai_mixed_json(raw)
211
+ if mixed_json:
212
+ return extract_openai_message(mixed_json)
213
+ streamed = parse_openai_sse(raw)
214
+ if streamed:
215
+ return streamed
216
+ raise RuntimeError(f"OpenAI-compatible returned non-JSON at {url} | response={raw[:500]}") from error
217
+ return extract_openai_message(data)
218
218
 
219
219
  def _call_anthropic(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
220
- api_key = env_value(endpoint.api_key_env) if endpoint.api_key_env else ""
220
+ api_key = env_value(endpoint.api_key_env) if endpoint.api_key_env else ""
221
221
  if endpoint.api_key_env and not api_key:
222
222
  raise ValueError(f"missing API key env: {endpoint.api_key_env}")
223
223
  base_url = endpoint.base_url.rstrip("/") if endpoint.base_url else "https://api.anthropic.com/v1"
@@ -252,13 +252,13 @@ class MultiModelCodingOrchestrator:
252
252
  ]
253
253
  )
254
254
  try:
255
- process = subprocess.run(
256
- [*args, "-p", subagent_prompt],
257
- check=False,
258
- stdin=subprocess.DEVNULL,
259
- capture_output=True,
260
- encoding="utf-8",
261
- timeout=300,
255
+ process = subprocess.run(
256
+ [*args, "-p", subagent_prompt],
257
+ check=False,
258
+ stdin=subprocess.DEVNULL,
259
+ capture_output=True,
260
+ encoding="utf-8",
261
+ timeout=300,
262
262
  )
263
263
  except FileNotFoundError as error:
264
264
  raise RuntimeError(f"Claude Code command not found: {args[0]}. Install Claude Code CLI or set base_url to the full command path.") from error
@@ -275,45 +275,46 @@ class MultiModelCodingOrchestrator:
275
275
  raise ValueError("codex command is required")
276
276
  if len(args) == 1:
277
277
  args.append("exec")
278
- codex_prompt = "\n".join(
279
- [
280
- f"You are AgentRAM {endpoint.role} agent running through Codex CLI.",
278
+ args = ensure_codex_skip_git_repo_check(args)
279
+ codex_prompt = "\n".join(
280
+ [
281
+ f"You are AgentRAM {endpoint.role} agent running through Codex CLI.",
281
282
  f"Model hint: {endpoint.model}",
282
283
  "If role is coder/main, inspect and edit files as needed in the current repo. If role is memory/small, do not edit files; write notes only.",
283
284
  "Return concise result, changed files, tests/verification, and blockers.",
284
285
  "",
285
- prompt,
286
- ]
287
- )
288
- prompt_path = ""
289
- try:
290
- with tempfile.NamedTemporaryFile(
291
- "w",
292
- encoding="utf-8",
293
- delete=False,
294
- dir=os.getcwd(),
295
- prefix=".agentram_codex_prompt_",
296
- suffix=".txt",
297
- ) as prompt_file:
298
- prompt_file.write(codex_prompt)
299
- prompt_path = prompt_file.name
300
- short_prompt = f"Read AgentRAM task from file and execute it: {prompt_path}"
301
- process = subprocess.run(
302
- [*args, short_prompt],
303
- check=False,
304
- stdin=subprocess.DEVNULL,
305
- capture_output=True,
306
- encoding="utf-8",
307
- timeout=int(os.getenv("AGENTRAM_CLI_TIMEOUT", "900")),
308
- )
286
+ prompt,
287
+ ]
288
+ )
289
+ prompt_path = ""
290
+ try:
291
+ with tempfile.NamedTemporaryFile(
292
+ "w",
293
+ encoding="utf-8",
294
+ delete=False,
295
+ dir=os.getcwd(),
296
+ prefix=".agentram_codex_prompt_",
297
+ suffix=".txt",
298
+ ) as prompt_file:
299
+ prompt_file.write(codex_prompt)
300
+ prompt_path = prompt_file.name
301
+ short_prompt = f"Read AgentRAM task from file and execute it: {prompt_path}"
302
+ process = subprocess.run(
303
+ [*args, short_prompt],
304
+ check=False,
305
+ stdin=subprocess.DEVNULL,
306
+ capture_output=True,
307
+ encoding="utf-8",
308
+ timeout=int(os.getenv("AGENTRAM_CLI_TIMEOUT", "900")),
309
+ )
309
310
  except FileNotFoundError as error:
310
- raise RuntimeError(f"Codex CLI command not found: {args[0]}. Install Codex CLI or set base_url to the full command path.") from error
311
- finally:
312
- if prompt_path:
313
- try:
314
- os.remove(prompt_path)
315
- except OSError:
316
- pass
311
+ raise RuntimeError(f"Codex CLI command not found: {args[0]}. Install Codex CLI or set base_url to the full command path.") from error
312
+ finally:
313
+ if prompt_path:
314
+ try:
315
+ os.remove(prompt_path)
316
+ except OSError:
317
+ pass
317
318
  output = (process.stdout or "").strip()
318
319
  error = (process.stderr or "").strip()
319
320
  if process.returncode != 0:
@@ -321,7 +322,15 @@ class MultiModelCodingOrchestrator:
321
322
  return output or error
322
323
 
323
324
 
324
- def parse_openai_sse(raw: str) -> str:
325
+ def ensure_codex_skip_git_repo_check(args: list[str]) -> list[str]:
326
+ if "--skip-git-repo-check" in args:
327
+ return args
328
+ if len(args) >= 2 and args[1] == "exec":
329
+ return [args[0], args[1], "--skip-git-repo-check", *args[2:]]
330
+ return args
331
+
332
+
333
+ def parse_openai_sse(raw: str) -> str:
325
334
  parts: list[str] = []
326
335
  for line in raw.splitlines():
327
336
  stripped = line.strip()
@@ -348,39 +357,39 @@ def parse_openai_sse(raw: str) -> str:
348
357
  message = choice.get("message", {})
349
358
  if isinstance(message, dict) and message.get("content"):
350
359
  parts.append(str(message.get("content")))
351
- return strip_thinking_blocks("".join(parts)).strip()
352
-
353
- def parse_openai_mixed_json(raw: str) -> dict[str, Any]:
354
- stripped = raw.strip()
355
- if not stripped.startswith("{"):
356
- return {}
357
- for marker in ("\ndata:", "\r\ndata:"):
358
- if marker not in stripped:
359
- continue
360
- candidate = stripped.split(marker, 1)[0].strip()
361
- try:
362
- value = json.loads(candidate)
363
- except json.JSONDecodeError:
364
- return {}
365
- return value if isinstance(value, dict) else {}
366
- return {}
367
-
368
- def extract_openai_message(data: dict[str, Any]) -> str:
369
- choices = data.get("choices", [])
370
- if not choices or not isinstance(choices, list):
371
- return ""
372
- first = choices[0]
373
- if not isinstance(first, dict):
374
- return ""
375
- message = first.get("message", {})
376
- if isinstance(message, dict):
377
- return strip_thinking_blocks(str(message.get("content", ""))).strip()
378
- delta = first.get("delta", {})
379
- if isinstance(delta, dict):
380
- return strip_thinking_blocks(str(delta.get("content", ""))).strip()
381
- return ""
382
-
383
- def strip_thinking_blocks(text: str) -> str:
360
+ return strip_thinking_blocks("".join(parts)).strip()
361
+
362
+ def parse_openai_mixed_json(raw: str) -> dict[str, Any]:
363
+ stripped = raw.strip()
364
+ if not stripped.startswith("{"):
365
+ return {}
366
+ for marker in ("\ndata:", "\r\ndata:"):
367
+ if marker not in stripped:
368
+ continue
369
+ candidate = stripped.split(marker, 1)[0].strip()
370
+ try:
371
+ value = json.loads(candidate)
372
+ except json.JSONDecodeError:
373
+ return {}
374
+ return value if isinstance(value, dict) else {}
375
+ return {}
376
+
377
+ def extract_openai_message(data: dict[str, Any]) -> str:
378
+ choices = data.get("choices", [])
379
+ if not choices or not isinstance(choices, list):
380
+ return ""
381
+ first = choices[0]
382
+ if not isinstance(first, dict):
383
+ return ""
384
+ message = first.get("message", {})
385
+ if isinstance(message, dict):
386
+ return strip_thinking_blocks(str(message.get("content", ""))).strip()
387
+ delta = first.get("delta", {})
388
+ if isinstance(delta, dict):
389
+ return strip_thinking_blocks(str(delta.get("content", ""))).strip()
390
+ return ""
391
+
392
+ def strip_thinking_blocks(text: str) -> str:
384
393
  text = re.sub(r"<thinking>.*?</thinking>", "", text, flags=re.DOTALL | re.IGNORECASE)
385
394
  text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL | re.IGNORECASE)
386
395
  text = re.sub(r"^\s*</?(thinking|think)>\s*", "", text, flags=re.IGNORECASE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentram",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
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.41"
7
+ version = "0.1.43"
8
8
  description = "Async, model-agnostic RAM layer for agentic coding tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"