agentram 0.1.30 → 0.1.32

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # AgentRAM
1
+ # AgentRAM
2
2
 
3
3
  Async, model-agnostic Working RAM for coding agents such as Codex, Claude Code, Cursor, and custom MCP clients.
4
4
 
@@ -407,7 +407,7 @@ TUI slash commands. Plain text only records/retrieves RAM; use `/ask` to call bo
407
407
  Storage:
408
408
 
409
409
  ```text
410
- .agentram/agent_profile.json
410
+ .agentram/agent_profile.jsonl
411
411
  ```
412
412
 
413
413
  ## MCP Tools
@@ -1051,3 +1051,4 @@ If auto install fails, install manually:
1051
1051
  ```powershell
1052
1052
  python -m pip install textual prompt_toolkit
1053
1053
  ```
1054
+
package/agentram/cli.py CHANGED
@@ -1,4 +1,4 @@
1
- from __future__ import annotations
1
+ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import asyncio
@@ -71,33 +71,19 @@ SUBAGENTS: dict[str, str] = {
71
71
 
72
72
  SLASH_COMMANDS: list[tuple[str, str]] = [
73
73
  ("/help", "Show commands"),
74
- ("/init", "Create .agentram files"),
74
+ ("/setup", "Open model setup"),
75
75
  ("/status", "Show RAM status"),
76
- ("/retrieve <query>", "Print relevant RAM context"),
76
+ ("/workflow", "Show supervisor/main/small agents"),
77
+ ("/models", "List bound workflow models"),
77
78
  ("/goal", "Show Goal Stack"),
78
- ("/set-goal key=value ...", "Update mission/current_goal/current_task/etc"),
79
- ("/drift <task>", "Check goal drift"),
79
+ ("/set-goal key=value ...", "Update goal/task"),
80
+ ("/retrieve <query>", "Print relevant RAM context"),
80
81
  ("/events", "Show latest events"),
81
82
  ("/replay", "Replay events into memory"),
82
- ("/agents", "List Claude-style subagent presets"),
83
- ("/agent <name> <task>", "Build Claude Code subagent prompt"),
84
- ("/models", "List bound coding models"),
85
- ("/remove-model <id>", "Remove bound coding model"),
86
- ("/disable-model <id>", "Disable bound coding model"),
87
- ("/clear-models", "Remove all bound coding models"),
88
- ("/bind-model provider model", "Bind model endpoint"),
89
- ("/bind-router provider model", "Bind intent router model"),
90
- ("/bind-agent slot provider model", "Bind supervisor/main/small agent"),
91
- ("/workflow", "Show supervisor workflow agents"),
92
- ("/setup", "Show model setup panel"),
93
- ("/route on|off|status", "Toggle auto prompt routing"),
94
- ("/orchestrate <task>", "Build multi-model agent plan"),
95
- ("/ask <prompt>", "Execute bound models and return outputs"),
96
- ("/claude-install [path]", "Install .claude commands and agents"),
97
83
  ("/note <text>", "Save decision note"),
98
84
  ("/clear", "Clear chat"),
99
85
  ("/exit", "Quit"),
100
- ("Plain text", "Capture coding chat message and retrieve context"),
86
+ ("Plain text", "Route prompt through AgentRAM runtime"),
101
87
  ]
102
88
 
103
89
 
@@ -222,7 +208,7 @@ CLAUDE_COMPAT_NAMES = {command.split()[0] for command, _ in CLAUDE_COMPAT_COMMAN
222
208
  def slash_entries() -> list[tuple[str, str]]:
223
209
  seen: set[str] = set()
224
210
  entries: list[tuple[str, str]] = []
225
- for command, description in [*SLASH_COMMANDS, *CLAUDE_COMPAT_COMMANDS]:
211
+ for command, description in SLASH_COMMANDS:
226
212
  name = command.split()[0]
227
213
  if name in seen:
228
214
  continue
@@ -232,33 +218,19 @@ def slash_entries() -> list[tuple[str, str]]:
232
218
 
233
219
  SLASH_HELP = """Slash commands:
234
220
  /help Show commands
235
- /init Create .agentram files
221
+ /setup Open model setup
236
222
  /status Show RAM status
237
- /retrieve <query> Print relevant RAM context
223
+ /workflow Show supervisor/main/small agents
224
+ /models List bound workflow models
238
225
  /goal Show Goal Stack
239
- /set-goal key=value ... Update mission/current_goal/current_task/etc
240
- /drift <task> Check goal drift
226
+ /set-goal key=value ... Update goal/task
227
+ /retrieve <query> Print relevant RAM context
241
228
  /events Show latest events
242
229
  /replay Replay events into memory
243
- /agents List subagent presets
244
- /agent <name> <task> Build Claude Code subagent prompt
245
- /models List bound coding models
246
- /remove-model <id> Remove bound coding model
247
- /disable-model <id> Disable bound coding model
248
- /clear-models Remove all bound coding models
249
- /bind-model provider model Bind model endpoint
250
- /bind-router provider model Bind intent router model
251
- /bind-agent slot provider model Bind supervisor/main/small agent
252
- /workflow Show supervisor workflow agents
253
- /setup Show model setup panel
254
- /route on|off|status Toggle auto prompt routing
255
- /orchestrate <task> Build multi-model agent plan
256
- /ask <prompt> Execute bound models and return outputs
257
- /claude-install [path] Install .claude commands and agents
258
230
  /note <text> Save decision note
259
231
  /clear Clear chat
260
232
  /exit Quit
261
- Plain text Capture coding chat message and retrieve context
233
+ Plain text Route prompt through AgentRAM runtime
262
234
  """.strip()
263
235
 
264
236
 
@@ -470,16 +442,18 @@ def run_textual_tui(context: McpContext) -> bool:
470
442
  #input { dock: bottom; border: solid #3b82f6; }
471
443
  .muted { color: #8b9bb0; }
472
444
  """
473
- BINDINGS = [("ctrl+c", "interrupt_or_quit", "Cancel/Quit"), ("tab", "accept_suggestion", "Complete slash"), ("ctrl+s", "save_setup", "Save setup"), ("up", "history_prev", "Previous prompt"), ("down", "history_next", "Next prompt")]
445
+ BINDINGS = [("ctrl+c", "interrupt_or_quit", "Cancel/Quit"), ("tab", "accept_suggestion", "Complete slash"), ("up", "history_prev", "Previous prompt"), ("down", "history_next", "Next prompt")]
474
446
 
475
447
  def __init__(self, mcp_context: McpContext) -> None:
476
448
  super().__init__()
477
449
  self.context = mcp_context
478
- self.messages: list[str] = []
479
- self.busy = False
480
- self.prompt_queue: list[str] = []
481
- self.processing_task: asyncio.Task[None] | None = None
482
- self.suggestions: list[str] = []
450
+ self.messages: list[str] = []
451
+ self.busy = False
452
+ self.prompt_queue: list[str] = []
453
+ self.processing_task: asyncio.Task[None] | None = None
454
+ self.pending_approval_prompt: str | None = None
455
+ self.project_main_agent_approved = False
456
+ self.suggestions: list[str] = []
483
457
  self.setup_done = False
484
458
  self.prompt_history: list[str] = []
485
459
  self.history_index: int | None = None
@@ -520,7 +494,6 @@ def run_textual_tui(context: McpContext) -> bool:
520
494
  with Horizontal(id="setup-actions"):
521
495
  yield Button("Back", id="setup-prev")
522
496
  yield Button("Next", id="setup-next", variant="primary")
523
- yield Button("Save setup", id="setup-save", variant="success")
524
497
  yield Button("Skip", id="setup-skip")
525
498
  yield RichLog(id="chat", wrap=True, highlight=True, markup=False, auto_scroll=True)
526
499
  yield Static(id="palette")
@@ -562,7 +535,7 @@ def run_textual_tui(context: McpContext) -> bool:
562
535
  self.query_one(f"#setup-{slot}-model", Input).value = endpoint.model
563
536
  self.query_one(f"#setup-{slot}-base-url", Input).value = endpoint.base_url
564
537
  self.query_one(f"#setup-{slot}-api-key-env", Input).value = endpoint.api_key_env
565
- self.chat.write("system > Existing workflow models loaded. Save to update, or Skip to keep.")
538
+ self.chat.write("system > Existing workflow models loaded. Next auto-saves each step, or Skip to keep.")
566
539
 
567
540
  def current_setup_slot(self) -> str:
568
541
  return self.setup_slots[self.setup_index]
@@ -582,12 +555,18 @@ def run_textual_tui(context: McpContext) -> bool:
582
555
  self.query_one(f"#setup-{self.current_setup_slot()}-model", Input).focus()
583
556
 
584
557
  def action_next_setup(self) -> None:
558
+ saved = self.save_setup_slot(self.current_setup_slot())
559
+ if saved:
560
+ self.chat.write(f"agentram > setup saved: {saved}")
585
561
  if self.setup_index < len(self.setup_slots) - 1:
586
562
  self.setup_index += 1
587
563
  self.refresh_setup_step()
588
564
  self.focus_current_setup_model()
589
565
  else:
590
- self.action_save_setup()
566
+ self.chat.write("agentram > setup complete")
567
+ self.hide_setup()
568
+ self.refresh_sidebar()
569
+ self.query_one("#input", Input).focus()
591
570
 
592
571
  def action_prev_setup(self) -> None:
593
572
  if self.setup_index > 0:
@@ -604,34 +583,32 @@ def run_textual_tui(context: McpContext) -> bool:
604
583
  return self.setup_value(f"setup-{slot}-custom-provider") or "custom"
605
584
  return preset
606
585
 
607
- def action_save_setup(self) -> None:
586
+ def save_setup_slot(self, slot: str) -> str:
608
587
  role_by_slot = {"supervisor": "supervisor", "main": "coder", "small": "memory"}
609
- bound: list[str] = []
610
- for slot, role in role_by_slot.items():
611
- provider = self.setup_provider(slot)
612
- model = self.setup_value(f"setup-{slot}-model")
613
- base_url = self.setup_value(f"setup-{slot}-base-url")
614
- api_key_input = self.setup_value(f"setup-{slot}-api-key-env")
615
- api_key_env, saved_env_name = resolve_api_key_env(slot, api_key_input)
616
- if not model:
617
- continue
618
- endpoint = AgentModelEndpoint(
619
- provider=provider,
620
- model=model,
621
- role=role,
622
- base_url=base_url,
623
- api_key_env=api_key_env,
624
- modalities=["text"],
625
- )
626
- self.context.profile_store.set_agent(slot, endpoint)
627
- if saved_env_name:
628
- bound.append(f"{slot}={provider}:{model} key=.env:{saved_env_name}")
629
- else:
630
- bound.append(f"{slot}={provider}:{model}")
631
- if not bound:
632
- self.chat.write("agentram > setup skipped: no model entered")
633
- else:
634
- self.chat.write("agentram > setup saved: " + ", ".join(bound))
588
+ role = role_by_slot[slot]
589
+ provider = self.setup_provider(slot)
590
+ model = self.setup_value(f"setup-{slot}-model")
591
+ base_url = self.setup_value(f"setup-{slot}-base-url")
592
+ api_key_input = self.setup_value(f"setup-{slot}-api-key-env")
593
+ api_key_env, saved_env_name = resolve_api_key_env(slot, api_key_input)
594
+ if not model:
595
+ return f"{slot}=skipped"
596
+ endpoint = AgentModelEndpoint(
597
+ provider=provider,
598
+ model=model,
599
+ role=role,
600
+ base_url=base_url,
601
+ api_key_env=api_key_env,
602
+ modalities=["text"],
603
+ )
604
+ self.context.profile_store.set_agent(slot, endpoint)
605
+ if saved_env_name:
606
+ return f"{slot}={provider}:{model} key=.env:{saved_env_name}"
607
+ return f"{slot}={provider}:{model}"
608
+
609
+ def action_save_setup(self) -> None:
610
+ bound = [self.save_setup_slot(slot) for slot in self.setup_slots]
611
+ self.chat.write("agentram > setup saved: " + ", ".join(bound))
635
612
  self.hide_setup()
636
613
  self.refresh_sidebar()
637
614
  self.query_one("#input", Input).focus()
@@ -684,9 +661,7 @@ def run_textual_tui(context: McpContext) -> bool:
684
661
  self.last_ctrl_c_at = now
685
662
 
686
663
  def on_button_pressed(self, event: Button.Pressed) -> None:
687
- if event.button.id == "setup-save":
688
- self.action_save_setup()
689
- elif event.button.id == "setup-next":
664
+ if event.button.id == "setup-next":
690
665
  self.action_next_setup()
691
666
  elif event.button.id == "setup-prev":
692
667
  self.action_prev_setup()
@@ -763,6 +738,27 @@ def run_textual_tui(context: McpContext) -> bool:
763
738
  return
764
739
  if text in BUTTONS:
765
740
  text = BUTTONS[text]
741
+ if self.pending_approval_prompt is not None:
742
+ decision = normalize_approval_decision(text)
743
+ if decision == "yes_once":
744
+ approved = self.pending_approval_prompt
745
+ self.pending_approval_prompt = None
746
+ self.chat.write("agentram > approved once")
747
+ self.enqueue_prompt(approved)
748
+ return
749
+ if decision == "yes_project":
750
+ approved = self.pending_approval_prompt
751
+ self.pending_approval_prompt = None
752
+ self.project_main_agent_approved = True
753
+ self.chat.write("agentram > approved for this project")
754
+ self.enqueue_prompt(approved)
755
+ return
756
+ if decision == "no":
757
+ self.chat.write("agentram > denied; prompt dropped")
758
+ self.pending_approval_prompt = None
759
+ return
760
+ self.chat.write("agentram > approval required: type yes, yes do this project, or no")
761
+ return
766
762
  if text and (not self.prompt_history or self.prompt_history[-1] != text):
767
763
  self.prompt_history.append(text)
768
764
  del self.prompt_history[:-100]
@@ -771,37 +767,49 @@ def run_textual_tui(context: McpContext) -> bool:
771
767
  self.chat.clear()
772
768
  self.chat.write("system > Chat cleared.")
773
769
  return
774
- if self.busy:
775
- self.prompt_queue.append(text)
776
- self.chat.write(f"agentram > queued #{len(self.prompt_queue)}")
777
- return
778
- self.prompt_queue.append(text)
779
- self.processing_task = asyncio.create_task(self.process_prompt_queue())
780
-
781
- async def process_prompt_queue(self) -> None:
782
- if self.busy:
783
- return
784
- self.busy = True
785
- try:
786
- while self.prompt_queue:
787
- text = self.prompt_queue.pop(0)
788
- pending = len(self.prompt_queue)
789
- suffix = f" ({pending} queued)" if pending else ""
790
- self.chat.write(f"agentram > thinking...{suffix}")
791
- try:
792
- response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
793
- except Exception as error: # noqa: BLE001 - TUI boundary
794
- response = f"error: {error}"
795
- if response == "__clear__":
796
- self.chat.clear()
797
- self.chat.write("system > Chat cleared.")
798
- elif response:
799
- self.chat.write("agentram > " + response)
800
- self.refresh_sidebar()
801
- finally:
802
- self.busy = False
803
- self.processing_task = None
804
- self.query_one("#input", Input).focus()
770
+ if requires_main_agent_approval(self.context, text) and not self.project_main_agent_approved:
771
+ self.pending_approval_prompt = text
772
+ self.chat.write(
773
+ "agentram > main agent wants terminal/file access. Allow?\n"
774
+ " yes = allow once\n"
775
+ " yes do this project = allow for this project\n"
776
+ " no = deny"
777
+ )
778
+ return
779
+ self.enqueue_prompt(text)
780
+
781
+ def enqueue_prompt(self, text: str) -> None:
782
+ if self.busy:
783
+ self.prompt_queue.append(text)
784
+ self.chat.write(f"agentram > queued #{len(self.prompt_queue)}")
785
+ return
786
+ self.prompt_queue.append(text)
787
+ self.processing_task = asyncio.create_task(self.process_prompt_queue())
788
+
789
+ async def process_prompt_queue(self) -> None:
790
+ if self.busy:
791
+ return
792
+ self.busy = True
793
+ try:
794
+ while self.prompt_queue:
795
+ text = self.prompt_queue.pop(0)
796
+ pending = len(self.prompt_queue)
797
+ suffix = f" ({pending} queued)" if pending else ""
798
+ self.chat.write(f"agentram > thinking...{suffix}")
799
+ try:
800
+ response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
801
+ except Exception as error: # noqa: BLE001 - TUI boundary
802
+ response = f"error: {error}"
803
+ if response == "__clear__":
804
+ self.chat.clear()
805
+ self.chat.write("system > Chat cleared.")
806
+ elif response:
807
+ self.chat.write("agentram > " + response)
808
+ self.refresh_sidebar()
809
+ finally:
810
+ self.busy = False
811
+ self.processing_task = None
812
+ self.query_one("#input", Input).focus()
805
813
 
806
814
  AgentRAMTextualApp(context).run()
807
815
  return True
@@ -844,7 +852,7 @@ def create_prompt_session() -> Any | None:
844
852
  completer=completer,
845
853
  complete_while_typing=True,
846
854
  reserve_space_for_menu=12,
847
- bottom_toolbar="Type / for slash commands | /agents for subagents | /ask to call bound models",
855
+ bottom_toolbar="Type / for AgentRAM commands | plain text routes through supervisor/main/small",
848
856
  )
849
857
 
850
858
 
@@ -853,6 +861,35 @@ def read_tui_input(prompt_session: Any | None) -> str:
853
861
  return input("You > ")
854
862
  return prompt_session.prompt("You > ")
855
863
 
864
+ def normalize_approval_decision(text: str) -> str:
865
+ normalized = text.strip().lower()
866
+ if normalized in {"yes", "y", "ok", "allow", "approve", "đồng ý", "dong y"}:
867
+ return "yes_once"
868
+ if normalized in {"yes do this project", "yes project", "always", "a", "p", "allow project", "approve project", "đồng ý project", "dong y project"}:
869
+ return "yes_project"
870
+ if normalized in {"no", "n", "deny", "reject", "không", "khong"}:
871
+ return "no"
872
+ return ""
873
+
874
+ def requires_main_agent_approval(context: McpContext, prompt: str) -> bool:
875
+ main_agent = context.profile_store.list_agents().get("main")
876
+ if not main_agent:
877
+ return False
878
+ provider = main_agent.provider.lower()
879
+ if provider not in {"codex-cli", "claude-code", "claude-cli", "claude-subagent", "claude-code-subagent"}:
880
+ return False
881
+ route = heuristic_agent_runtime_route(prompt)
882
+ if str(route.get("intent")) == "planning":
883
+ return False
884
+ if str(route.get("intent")) == "execution":
885
+ return True
886
+ lower = prompt.lower()
887
+ risky_words = [
888
+ "terminal", "command", "shell", "powershell", "cmd", "npm", "pip", "git", "pytest", "run", "build",
889
+ "sửa", "sua", "fix", "refactor", "edit", "write", "delete", "xóa", "xoa", "tạo file", "tao file",
890
+ ]
891
+ return any(word in lower for word in risky_words)
892
+
856
893
  def handle_chat_input(context: McpContext, text: str, messages: list[str] | None = None) -> str:
857
894
  if not text:
858
895
  return SLASH_HELP
@@ -1198,192 +1235,192 @@ def workflow_text(context: McpContext) -> str:
1198
1235
  lines.append(f"- {slot}: not bound")
1199
1236
  return "\n".join(lines)
1200
1237
 
1201
- def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
1202
- agents = context.profile_store.list_agents()
1203
- supervisor = agents.get("supervisor")
1204
- main_agent = agents.get("main")
1205
- small_agent = agents.get("small")
1206
- memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
1207
- route = classify_agent_runtime_route(context, prompt, main_agent)
1208
- intent = str(route.get("intent", "chat"))
1209
- outputs: dict[str, str] = {}
1210
-
1211
- if intent == "planning":
1212
- plan = supervisor_plan(supervisor, prompt, memory_context)
1213
- outputs["supervisor"] = plan
1214
- outputs["small"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
1215
- sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1216
- persist_workflow_memory(context, prompt, plan, outputs)
1217
- return "\n".join(
1218
- [
1219
- f"Workflow: route=planning confidence={float(route.get('confidence', 0.0)):.2f} -> supervisor -> small memory note -> merge",
1220
- "Supervisor plan:",
1221
- plan,
1222
- "",
1223
- "Small memory agent notes:",
1224
- outputs.get("small", "No small agent bound."),
1225
- ]
1226
- )
1227
-
1228
- if intent == "memory":
1229
- plan = "Memory-only event. Small agent writes RAM note directly."
1230
- outputs["small"] = small_agent_observe(context, small_agent, prompt, "memory_event", prompt, plan, memory_context)
1231
- sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1232
- persist_workflow_memory(context, prompt, plan, outputs)
1233
- return "\n".join(
1234
- [
1235
- f"Workflow: route=memory confidence={float(route.get('confidence', 0.0)):.2f} -> small memory note -> merge",
1236
- "Small memory agent notes:",
1237
- outputs.get("small", "No small agent bound."),
1238
- ]
1239
- )
1240
-
1241
- plan = runtime_execution_plan(context, supervisor, prompt, memory_context, route)
1242
- outputs["supervisor"] = plan
1243
- outputs["small_after_plan"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
1244
- if main_agent:
1245
- try:
1246
- outputs["main"] = call_agent_endpoint(main_agent, build_main_agent_prompt(prompt, plan, memory_context))
1247
- except Exception as error: # noqa: BLE001 - agent boundary
1248
- outputs["main"] = f"error: {model_error_hint(str(error))}"
1249
- else:
1250
- outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
1251
- outputs["small"] = small_agent_observe(context, small_agent, prompt, "main_result", outputs.get("main", ""), plan, memory_context)
1252
- sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1253
- persist_workflow_memory(context, prompt, plan, outputs)
1254
- return "\n".join(
1255
- [
1256
- f"Workflow: route={intent} confidence={float(route.get('confidence', 0.0)):.2f} -> main -> small memory note -> merge",
1257
- "Supervisor plan:",
1258
- plan,
1259
- "",
1260
- "Main agent result:",
1261
- outputs.get("main", "-"),
1262
- "",
1263
- "Small memory agent after plan:",
1264
- outputs.get("small_after_plan", "No small agent bound."),
1265
- "",
1266
- "Small memory agent notes:",
1267
- outputs.get("small", "No small agent bound."),
1268
- ]
1269
- )
1270
-
1271
- def classify_agent_runtime_route(context: McpContext, prompt: str, main_agent: AgentModelEndpoint | None = None) -> dict[str, object]:
1272
- if main_agent:
1273
- try:
1274
- response = call_agent_endpoint(main_agent, build_agent_runtime_router_prompt(prompt))
1275
- data = json.loads(extract_json_object(response))
1276
- intent = str(data.get("intent", "")).strip().lower()
1277
- if intent in {"planning", "execution", "memory", "chat"}:
1278
- return {
1279
- "intent": intent,
1280
- "confidence": float(data.get("confidence", 0.75)),
1281
- "reason": str(data.get("reason", "main-agent router")),
1282
- "source": "main-agent",
1283
- }
1284
- except Exception as error: # noqa: BLE001 - router boundary
1285
- fallback = heuristic_agent_runtime_route(prompt)
1286
- fallback["source"] = "heuristic"
1287
- fallback["router_error"] = str(error)
1288
- return fallback
1289
-
1290
- return heuristic_agent_runtime_route(prompt)
1291
-
1292
- def heuristic_agent_runtime_route(prompt: str) -> dict[str, object]:
1293
- lower = prompt.lower()
1294
- execute_words = [
1295
- "bắt đầu làm", "bat dau lam", "làm đi", "lam di", "tiến hành", "tien hanh", "thực hiện", "thuc hien",
1296
- "chốt kế hoạch", "chot ke hoach", "bắt tay", "start", "execute", "implement", "do it", "go ahead",
1297
- ]
1298
- plan_words = ["lên kế hoạch", "len ke hoach", "kế hoạch", "ke hoach", "plan", "roadmap", "milestone", "thiết kế", "thiet ke"]
1299
- if any(word in lower for word in execute_words):
1300
- return {"intent": "execution", "confidence": 0.9, "source": "heuristic"}
1301
- if any(word in lower for word in plan_words):
1302
- return {"intent": "planning", "confidence": 0.85, "source": "heuristic"}
1303
- decision = heuristic_intent(prompt)
1304
- intent = str(decision.get("intent", "chat"))
1305
- if intent == "coding":
1306
- decision["intent"] = "execution"
1307
- decision["source"] = "heuristic"
1308
- return decision
1309
-
1310
- def build_agent_runtime_router_prompt(prompt: str) -> str:
1311
- return "\n".join(
1312
- [
1313
- "You are AgentRAM orchestration router using the main agent model.",
1314
- "Classify which agent should handle the user prompt. Do not edit files. Do not run tools. Return only JSON.",
1315
- "Schema: {\"intent\":\"planning|execution|memory|chat\",\"confidence\":0.0,\"reason\":\"short\"}",
1316
- "planning = user asks to plan, design, set roadmap, discuss goal, no execution yet.",
1317
- "execution = user approves plan, says start/do it, or asks coding/refactor/fix/test/file work.",
1318
- "memory = user asks to remember/save/note a decision/fact.",
1319
- "chat = normal conversation.",
1320
- "Nếu user prompt bằng tiếng Việt, reason dùng tiếng Việt.",
1321
- "User prompt:",
1322
- prompt,
1323
- ]
1324
- )
1325
-
1326
- def runtime_execution_plan(context: McpContext, supervisor: AgentModelEndpoint | None, prompt: str, memory_context: str, route: dict[str, object]) -> str:
1327
- state = context.goal_store.load().context_block()
1328
- if state:
1329
- return "\n".join(["Use current accepted Goal Stack as execution plan.", state])
1330
- if str(route.get("intent")) == "execution" and any(word in prompt.lower() for word in ["bắt đầu", "bat dau", "làm đi", "lam di", "execute", "do it"]):
1331
- return "User approved previous plan. Use AgentRAM memory context and current prompt to execute next concrete task."
1332
- return supervisor_plan(supervisor, prompt, memory_context)
1333
-
1334
- def small_agent_observe(context: McpContext, small_agent: AgentModelEndpoint | None, prompt: str, event_type: str, event_content: str, plan: str, memory_context: str) -> str:
1335
- if not small_agent:
1336
- return "No small agent bound."
1337
- try:
1338
- return call_agent_endpoint(
1339
- small_agent,
1340
- build_small_agent_prompt(prompt, plan, memory_context, event_content, event_type=event_type),
1341
- )
1342
- except Exception as error: # noqa: BLE001 - agent boundary
1343
- return f"error: {model_error_hint(str(error))}"
1344
-
1345
- def sync_goal_state_from_small_agent(context: McpContext, prompt: str, plan: str, small_notes: str) -> None:
1346
- if not small_notes.strip() or small_notes.strip().startswith("error:"):
1347
- return
1348
- state = context.goal_store.load()
1349
- goal = extract_labeled_value(small_notes, ("current_goal", "goal", "mục tiêu", "muc tieu")) or first_nonempty_line(plan)
1350
- task = extract_labeled_value(small_notes, ("current_task", "task", "nhiệm vụ", "nhiem vu")) or first_nonempty_line(small_notes) or prompt
1351
- patch: dict[str, object] = {
1352
- "current_task": trim(task.strip(), 180),
1353
- "goal_history": [trim(goal.strip() or prompt.strip(), 180)],
1354
- }
1355
- if not state.mission:
1356
- patch["mission"] = "Xây dựng và vận hành AgentRAM cho coding agents."
1357
- if not state.project_goal:
1358
- patch["project_goal"] = "AgentRAM workflow: supervisor lập kế hoạch, main coding agent thực thi, small memory agent ghi RAM."
1359
- if goal:
1360
- patch["current_goal"] = trim(goal.strip(), 180)
1361
- context.goal_store.update(patch)
1362
-
1363
- def extract_labeled_value(text: str, labels: tuple[str, ...]) -> str:
1364
- for line in text.splitlines():
1365
- cleaned = line.strip().lstrip("-*").strip()
1366
- for label in labels:
1367
- pattern = rf"^{re.escape(label)}\s*[:=]\s*(.+)$"
1368
- match = re.match(pattern, cleaned, flags=re.IGNORECASE)
1369
- if match:
1370
- return match.group(1).strip().strip("`*_-")
1371
- return ""
1372
-
1373
- def first_nonempty_line(value: str) -> str:
1374
- for line in value.splitlines():
1375
- cleaned = line.strip().lstrip("-0123456789. )")
1376
- if cleaned and not cleaned.lower().startswith(("supervisor error", "fallback plan")):
1377
- return cleaned
1378
- return ""
1379
-
1380
- def persist_workflow_memory(context: McpContext, prompt: str, plan: str, outputs: dict[str, str]) -> None:
1381
- small_notes = "\n".join(
1382
- value.strip()
1383
- for key, value in outputs.items()
1384
- if key.startswith("small") and value.strip() and not value.strip().startswith("error:")
1385
- )
1386
- main_result = outputs.get("main", "").strip()
1238
+ def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
1239
+ agents = context.profile_store.list_agents()
1240
+ supervisor = agents.get("supervisor")
1241
+ main_agent = agents.get("main")
1242
+ small_agent = agents.get("small")
1243
+ memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
1244
+ route = classify_agent_runtime_route(context, prompt, main_agent)
1245
+ intent = str(route.get("intent", "chat"))
1246
+ outputs: dict[str, str] = {}
1247
+
1248
+ if intent == "planning":
1249
+ plan = supervisor_plan(supervisor, prompt, memory_context)
1250
+ outputs["supervisor"] = plan
1251
+ outputs["small"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
1252
+ sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1253
+ persist_workflow_memory(context, prompt, plan, outputs)
1254
+ return "\n".join(
1255
+ [
1256
+ f"Workflow: route=planning confidence={float(route.get('confidence', 0.0)):.2f} -> supervisor -> small memory note -> merge",
1257
+ "Supervisor plan:",
1258
+ plan,
1259
+ "",
1260
+ "Small memory agent notes:",
1261
+ outputs.get("small", "No small agent bound."),
1262
+ ]
1263
+ )
1264
+
1265
+ if intent == "memory":
1266
+ plan = "Memory-only event. Small agent writes RAM note directly."
1267
+ outputs["small"] = small_agent_observe(context, small_agent, prompt, "memory_event", prompt, plan, memory_context)
1268
+ sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1269
+ persist_workflow_memory(context, prompt, plan, outputs)
1270
+ return "\n".join(
1271
+ [
1272
+ f"Workflow: route=memory confidence={float(route.get('confidence', 0.0)):.2f} -> small memory note -> merge",
1273
+ "Small memory agent notes:",
1274
+ outputs.get("small", "No small agent bound."),
1275
+ ]
1276
+ )
1277
+
1278
+ plan = runtime_execution_plan(context, supervisor, prompt, memory_context, route)
1279
+ outputs["supervisor"] = plan
1280
+ outputs["small_after_plan"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
1281
+ if main_agent:
1282
+ try:
1283
+ outputs["main"] = call_agent_endpoint(main_agent, build_main_agent_prompt(prompt, plan, memory_context))
1284
+ except Exception as error: # noqa: BLE001 - agent boundary
1285
+ outputs["main"] = f"error: {model_error_hint(str(error))}"
1286
+ else:
1287
+ outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
1288
+ outputs["small"] = small_agent_observe(context, small_agent, prompt, "main_result", outputs.get("main", ""), plan, memory_context)
1289
+ sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1290
+ persist_workflow_memory(context, prompt, plan, outputs)
1291
+ return "\n".join(
1292
+ [
1293
+ f"Workflow: route={intent} confidence={float(route.get('confidence', 0.0)):.2f} -> main -> small memory note -> merge",
1294
+ "Supervisor plan:",
1295
+ plan,
1296
+ "",
1297
+ "Main agent result:",
1298
+ outputs.get("main", "-"),
1299
+ "",
1300
+ "Small memory agent after plan:",
1301
+ outputs.get("small_after_plan", "No small agent bound."),
1302
+ "",
1303
+ "Small memory agent notes:",
1304
+ outputs.get("small", "No small agent bound."),
1305
+ ]
1306
+ )
1307
+
1308
+ def classify_agent_runtime_route(context: McpContext, prompt: str, main_agent: AgentModelEndpoint | None = None) -> dict[str, object]:
1309
+ if main_agent:
1310
+ try:
1311
+ response = call_agent_endpoint(main_agent, build_agent_runtime_router_prompt(prompt))
1312
+ data = json.loads(extract_json_object(response))
1313
+ intent = str(data.get("intent", "")).strip().lower()
1314
+ if intent in {"planning", "execution", "memory", "chat"}:
1315
+ return {
1316
+ "intent": intent,
1317
+ "confidence": float(data.get("confidence", 0.75)),
1318
+ "reason": str(data.get("reason", "main-agent router")),
1319
+ "source": "main-agent",
1320
+ }
1321
+ except Exception as error: # noqa: BLE001 - router boundary
1322
+ fallback = heuristic_agent_runtime_route(prompt)
1323
+ fallback["source"] = "heuristic"
1324
+ fallback["router_error"] = str(error)
1325
+ return fallback
1326
+
1327
+ return heuristic_agent_runtime_route(prompt)
1328
+
1329
+ def heuristic_agent_runtime_route(prompt: str) -> dict[str, object]:
1330
+ lower = prompt.lower()
1331
+ execute_words = [
1332
+ "bắt đầu làm", "bat dau lam", "làm đi", "lam di", "tiến hành", "tien hanh", "thực hiện", "thuc hien",
1333
+ "chốt kế hoạch", "chot ke hoach", "bắt tay", "start", "execute", "implement", "do it", "go ahead",
1334
+ ]
1335
+ plan_words = ["lên kế hoạch", "len ke hoach", "kế hoạch", "ke hoach", "plan", "roadmap", "milestone", "thiết kế", "thiet ke"]
1336
+ if any(word in lower for word in execute_words):
1337
+ return {"intent": "execution", "confidence": 0.9, "source": "heuristic"}
1338
+ if any(word in lower for word in plan_words):
1339
+ return {"intent": "planning", "confidence": 0.85, "source": "heuristic"}
1340
+ decision = heuristic_intent(prompt)
1341
+ intent = str(decision.get("intent", "chat"))
1342
+ if intent == "coding":
1343
+ decision["intent"] = "execution"
1344
+ decision["source"] = "heuristic"
1345
+ return decision
1346
+
1347
+ def build_agent_runtime_router_prompt(prompt: str) -> str:
1348
+ return "\n".join(
1349
+ [
1350
+ "You are AgentRAM orchestration router using the main agent model.",
1351
+ "Classify which agent should handle the user prompt. Do not edit files. Do not run tools. Return only JSON.",
1352
+ "Schema: {\"intent\":\"planning|execution|memory|chat\",\"confidence\":0.0,\"reason\":\"short\"}",
1353
+ "planning = user asks to plan, design, set roadmap, discuss goal, no execution yet.",
1354
+ "execution = user approves plan, says start/do it, or asks coding/refactor/fix/test/file work.",
1355
+ "memory = user asks to remember/save/note a decision/fact.",
1356
+ "chat = normal conversation.",
1357
+ "Nếu user prompt bằng tiếng Việt, reason dùng tiếng Việt.",
1358
+ "User prompt:",
1359
+ prompt,
1360
+ ]
1361
+ )
1362
+
1363
+ def runtime_execution_plan(context: McpContext, supervisor: AgentModelEndpoint | None, prompt: str, memory_context: str, route: dict[str, object]) -> str:
1364
+ state = context.goal_store.load().context_block()
1365
+ if state:
1366
+ return "\n".join(["Use current accepted Goal Stack as execution plan.", state])
1367
+ if str(route.get("intent")) == "execution" and any(word in prompt.lower() for word in ["bắt đầu", "bat dau", "làm đi", "lam di", "execute", "do it"]):
1368
+ return "User approved previous plan. Use AgentRAM memory context and current prompt to execute next concrete task."
1369
+ return supervisor_plan(supervisor, prompt, memory_context)
1370
+
1371
+ def small_agent_observe(context: McpContext, small_agent: AgentModelEndpoint | None, prompt: str, event_type: str, event_content: str, plan: str, memory_context: str) -> str:
1372
+ if not small_agent:
1373
+ return "No small agent bound."
1374
+ try:
1375
+ return call_agent_endpoint(
1376
+ small_agent,
1377
+ build_small_agent_prompt(prompt, plan, memory_context, event_content, event_type=event_type),
1378
+ )
1379
+ except Exception as error: # noqa: BLE001 - agent boundary
1380
+ return f"error: {model_error_hint(str(error))}"
1381
+
1382
+ def sync_goal_state_from_small_agent(context: McpContext, prompt: str, plan: str, small_notes: str) -> None:
1383
+ if not small_notes.strip() or small_notes.strip().startswith("error:"):
1384
+ return
1385
+ state = context.goal_store.load()
1386
+ goal = extract_labeled_value(small_notes, ("current_goal", "goal", "mục tiêu", "muc tieu")) or first_nonempty_line(plan)
1387
+ task = extract_labeled_value(small_notes, ("current_task", "task", "nhiệm vụ", "nhiem vu")) or first_nonempty_line(small_notes) or prompt
1388
+ patch: dict[str, object] = {
1389
+ "current_task": trim(task.strip(), 180),
1390
+ "goal_history": [trim(goal.strip() or prompt.strip(), 180)],
1391
+ }
1392
+ if not state.mission:
1393
+ patch["mission"] = "Xây dựng và vận hành AgentRAM cho coding agents."
1394
+ if not state.project_goal:
1395
+ patch["project_goal"] = "AgentRAM workflow: supervisor lập kế hoạch, main coding agent thực thi, small memory agent ghi RAM."
1396
+ if goal:
1397
+ patch["current_goal"] = trim(goal.strip(), 180)
1398
+ context.goal_store.update(patch)
1399
+
1400
+ def extract_labeled_value(text: str, labels: tuple[str, ...]) -> str:
1401
+ for line in text.splitlines():
1402
+ cleaned = line.strip().lstrip("-*").strip()
1403
+ for label in labels:
1404
+ pattern = rf"^{re.escape(label)}\s*[:=]\s*(.+)$"
1405
+ match = re.match(pattern, cleaned, flags=re.IGNORECASE)
1406
+ if match:
1407
+ return match.group(1).strip().strip("`*_-")
1408
+ return ""
1409
+
1410
+ def first_nonempty_line(value: str) -> str:
1411
+ for line in value.splitlines():
1412
+ cleaned = line.strip().lstrip("-0123456789. )")
1413
+ if cleaned and not cleaned.lower().startswith(("supervisor error", "fallback plan")):
1414
+ return cleaned
1415
+ return ""
1416
+
1417
+ def persist_workflow_memory(context: McpContext, prompt: str, plan: str, outputs: dict[str, str]) -> None:
1418
+ small_notes = "\n".join(
1419
+ value.strip()
1420
+ for key, value in outputs.items()
1421
+ if key.startswith("small") and value.strip() and not value.strip().startswith("error:")
1422
+ )
1423
+ main_result = outputs.get("main", "").strip()
1387
1424
  note_parts = [
1388
1425
  f"User request: {prompt}",
1389
1426
  f"Supervisor plan: {trim(plan.strip(), 1200)}" if plan.strip() else "Supervisor plan: -",
@@ -1414,14 +1451,14 @@ def supervisor_plan(supervisor: AgentModelEndpoint | None, prompt: str, memory_c
1414
1451
  def call_agent_endpoint(endpoint: AgentModelEndpoint, prompt: str) -> str:
1415
1452
  return MultiModelCodingOrchestrator([endpoint])._call_endpoint(endpoint, prompt)
1416
1453
 
1417
- def build_supervisor_prompt(prompt: str, memory_context: str) -> str:
1418
- return "\n".join(["You are AgentRAM supervisor. Act like a senior coding-agent boss.", "Nếu user prompt bằng tiếng Việt, trả lời bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "Create a concise plan only. Do not code. Main agent runs later only after user approves/starts execution.", "Small memory agent observes this plan and writes RAM notes.", "Return only actionable plan. No markdown table.", "User request:", prompt, "AgentRAM context:", memory_context])
1419
-
1420
- def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
1421
- return "\n".join(["You are AgentRAM main coding agent.", "Nếu user prompt bằng tiếng Việt, trả lời bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "Follow supervisor plan. Focus on planning, orchestration, coding, refactor, bug fix, and final result.", "Do not invent repo facts. Mention files to inspect/change.", "User request:", prompt, "Supervisor plan:", plan, "AgentRAM context:", memory_context])
1422
-
1423
- def build_small_agent_prompt(prompt: str, plan: str, memory_context: str, event_content: str = "", event_type: str = "main_result") -> str:
1424
- return "\n".join(["You are AgentRAM small memory agent.", "Nếu user prompt bằng tiếng Việt, ghi note RAM bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "You participate in every AgentRAM event directly: user request, supervisor plan, main result, compact/merge.", "Do not edit code. Observe event content, then note decisions, risks, files, next tasks, and coding facts for RAM.", "You are responsible for setting the visible RAM goal/task from the latest event.", "Return concise memory notes only. Không dùng tiếng Anh nếu user đang dùng tiếng Việt.", "Bắt buộc gồm 2 dòng đầu theo format: current_goal: ... và current_task: ...", "User request:", prompt, "Supervisor plan:", plan, "Observed event type:", event_type, "Observed event content:", event_content or "-", "AgentRAM context:", memory_context])
1454
+ def build_supervisor_prompt(prompt: str, memory_context: str) -> str:
1455
+ return "\n".join(["You are AgentRAM supervisor. Act like a senior coding-agent boss.", "Nếu user prompt bằng tiếng Việt, trả lời bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "Create a concise plan only. Do not code. Main agent runs later only after user approves/starts execution.", "Small memory agent observes this plan and writes RAM notes.", "Return only actionable plan. No markdown table.", "User request:", prompt, "AgentRAM context:", memory_context])
1456
+
1457
+ def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
1458
+ return "\n".join(["You are AgentRAM main coding agent.", "Nếu user prompt bằng tiếng Việt, trả lời bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "Follow supervisor plan. Focus on planning, orchestration, coding, refactor, bug fix, and final result.", "Do not invent repo facts. Mention files to inspect/change.", "User request:", prompt, "Supervisor plan:", plan, "AgentRAM context:", memory_context])
1459
+
1460
+ def build_small_agent_prompt(prompt: str, plan: str, memory_context: str, event_content: str = "", event_type: str = "main_result") -> str:
1461
+ return "\n".join(["You are AgentRAM small memory agent.", "Nếu user prompt bằng tiếng Việt, ghi note RAM bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "You participate in every AgentRAM event directly: user request, supervisor plan, main result, compact/merge.", "Do not edit code. Observe event content, then note decisions, risks, files, next tasks, and coding facts for RAM.", "You are responsible for setting the visible RAM goal/task from the latest event.", "Return concise memory notes only. Không dùng tiếng Anh nếu user đang dùng tiếng Việt.", "Bắt buộc gồm 2 dòng đầu theo format: current_goal: ... và current_task: ...", "User request:", prompt, "Supervisor plan:", plan, "Observed event type:", event_type, "Observed event content:", event_content or "-", "AgentRAM context:", memory_context])
1425
1462
 
1426
1463
  def bind_router_text(context: McpContext, options: dict[str, object]) -> str:
1427
1464
  api_key_env = str(options.get("api_key_env", ""))
@@ -1552,7 +1589,7 @@ def model_error_hint(message: str) -> str:
1552
1589
  if "returned non-json" in lower or "expecting value" in lower:
1553
1590
  return message + " | Fix: endpoint returned empty/HTML/non-JSON. Check base_url, auth key, proxy, and server logs."
1554
1591
  if "command not found" in lower or "winerror 2" in lower or "the system cannot find the file" in lower:
1555
- return message + r" | Fix: install the CLI or set Main base_url to full command path. For Codex use base_url='C:\Users\admin\AppData\Roaming\npm\codex.cmd exec' or paste codex.cmd path; AgentRAM auto-adds exec when missing."
1592
+ return message + r" | Fix: install the CLI or set Main base_url to full command path. For Codex use base_url='C:\Users\admin\AppData\Roaming\npm\codex.cmd exec' or paste codex.cmd path; AgentRAM auto-adds exec when missing."
1556
1593
  if "missing api key env" in lower:
1557
1594
  return message + " | Fix: set environment variable, then bind using --api-key-env ENV_NAME, not raw key."
1558
1595
  return message
@@ -1665,20 +1702,14 @@ def sidebar_lines(context: McpContext) -> list[str]:
1665
1702
  "TASK",
1666
1703
  f" {trim(goal.current_task or '-', 24)}",
1667
1704
  "",
1668
- "BUTTONS",
1669
- " 1 init 2 status",
1670
- " 3 retrieve 4 goal",
1671
- " 5 set-goal 6 drift",
1672
- " 7 events 8 replay",
1673
- " 9 help 0 exit",
1674
- "",
1675
- "CLAUDE",
1705
+ "COMMANDS",
1676
1706
  " / = command palette",
1677
- " /agents",
1678
- " /claude-install .",
1679
- " /models",
1680
- " /clear-models",
1681
- " /ask <prompt>",
1707
+ " /setup",
1708
+ " /status",
1709
+ " /goal",
1710
+ " /workflow",
1711
+ " /note <text>",
1712
+ " /clear",
1682
1713
  ]
1683
1714
 
1684
1715
 
@@ -46,7 +46,7 @@ class McpContext:
46
46
 
47
47
  @property
48
48
  def profile_store(self) -> JsonAgentProfileStore:
49
- return JsonAgentProfileStore(self.ram_root / "agent_profile.json")
49
+ return JsonAgentProfileStore(self.ram_root / "agent_profile.jsonl")
50
50
 
51
51
  @property
52
52
  def merger(self) -> MemoryMerger:
@@ -252,12 +252,13 @@ class MultiModelCodingOrchestrator:
252
252
  ]
253
253
  )
254
254
  try:
255
- process = subprocess.run(
256
- [*args, "-p", subagent_prompt],
257
- check=False,
258
- capture_output=True,
259
- encoding="utf-8",
260
- 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,
261
262
  )
262
263
  except FileNotFoundError as error:
263
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
@@ -300,6 +301,7 @@ class MultiModelCodingOrchestrator:
300
301
  process = subprocess.run(
301
302
  [*args, short_prompt],
302
303
  check=False,
304
+ stdin=subprocess.DEVNULL,
303
305
  capture_output=True,
304
306
  encoding="utf-8",
305
307
  timeout=int(os.getenv("AGENTRAM_CLI_TIMEOUT", "900")),
@@ -80,27 +80,48 @@ class JsonAgentProfileStore:
80
80
  self.path = Path(path)
81
81
  self.path.parent.mkdir(parents=True, exist_ok=True)
82
82
 
83
- def load(self) -> dict[str, object]:
84
- if not self.path.exists():
85
- return {"updated_at": utc_now(), "endpoints": [], "router": None, "auto_route": True, "agents": {}}
86
- data = json.loads(self.path.read_text(encoding="utf-8"))
87
- data.setdefault("endpoints", [])
88
- data.setdefault("router", None)
89
- data.setdefault("auto_route", True)
90
- data.setdefault("agents", {})
91
- return data
92
-
93
- def save(self, endpoints: list[AgentModelEndpoint]) -> dict[str, object]:
94
- current = self.load()
95
- payload = {
96
- "updated_at": utc_now(),
97
- "endpoints": [endpoint.to_dict() for endpoint in endpoints],
98
- "router": current.get("router"),
99
- "auto_route": bool(current.get("auto_route", True)),
100
- "agents": current.get("agents", {}),
101
- }
102
- self.path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
103
- return payload
83
+ def default_payload(self) -> dict[str, object]:
84
+ return {"updated_at": utc_now(), "endpoints": [], "router": None, "auto_route": True, "agents": {}}
85
+
86
+ def legacy_json_path(self) -> Path:
87
+ return self.path.with_suffix(".json") if self.path.suffix == ".jsonl" else self.path
88
+
89
+ def normalize_payload(self, data: dict[str, object]) -> dict[str, object]:
90
+ data.setdefault("endpoints", [])
91
+ data.setdefault("router", None)
92
+ data.setdefault("auto_route", True)
93
+ data.setdefault("agents", {})
94
+ return data
95
+
96
+ def load(self) -> dict[str, object]:
97
+ if self.path.exists():
98
+ lines = [line.strip() for line in self.path.read_text(encoding="utf-8").splitlines() if line.strip()]
99
+ if not lines:
100
+ return self.default_payload()
101
+ return self.normalize_payload(json.loads(lines[-1]))
102
+ legacy = self.legacy_json_path()
103
+ if legacy.exists() and legacy != self.path:
104
+ data = self.normalize_payload(json.loads(legacy.read_text(encoding="utf-8")))
105
+ self.write_snapshot(data)
106
+ return data
107
+ return self.default_payload()
108
+
109
+ def write_snapshot(self, payload: dict[str, object]) -> None:
110
+ self.path.parent.mkdir(parents=True, exist_ok=True)
111
+ with self.path.open("a", encoding="utf-8") as file:
112
+ file.write(json.dumps(payload, ensure_ascii=False) + "\n")
113
+
114
+ def save(self, endpoints: list[AgentModelEndpoint]) -> dict[str, object]:
115
+ current = self.load()
116
+ payload = {
117
+ "updated_at": utc_now(),
118
+ "endpoints": [endpoint.to_dict() for endpoint in endpoints],
119
+ "router": current.get("router"),
120
+ "auto_route": bool(current.get("auto_route", True)),
121
+ "agents": current.get("agents", {}),
122
+ }
123
+ self.write_snapshot(payload)
124
+ return payload
104
125
 
105
126
  def list_endpoints(self) -> list[AgentModelEndpoint]:
106
127
  data = self.load()
@@ -143,62 +164,62 @@ class JsonAgentProfileStore:
143
164
  payload["changed"] = changed
144
165
  return payload
145
166
 
146
- def clear(self) -> dict[str, object]:
147
- payload = self.save([])
148
- payload["cleared"] = True
149
- return payload
150
-
151
- def get_router(self) -> AgentModelEndpoint | None:
152
- router = self.load().get("router")
153
- if not isinstance(router, dict):
154
- return None
155
- return AgentModelEndpoint.from_dict(router)
156
-
157
- def set_router(self, endpoint: AgentModelEndpoint | None) -> dict[str, object]:
158
- current = self.load()
159
- current["updated_at"] = utc_now()
160
- current["router"] = endpoint.to_dict() if endpoint else None
161
- self.path.write_text(json.dumps(current, indent=2, ensure_ascii=False), encoding="utf-8")
162
- return current
163
-
164
- def auto_route_enabled(self) -> bool:
165
- return bool(self.load().get("auto_route", True))
166
-
167
- def set_auto_route(self, enabled: bool) -> dict[str, object]:
168
- current = self.load()
169
- current["updated_at"] = utc_now()
170
- current["auto_route"] = enabled
171
- self.path.write_text(json.dumps(current, indent=2, ensure_ascii=False), encoding="utf-8")
172
- return current
173
-
174
- def get_agent(self, slot: str) -> AgentModelEndpoint | None:
175
- agents = self.load().get("agents", {})
176
- if not isinstance(agents, dict):
177
- return None
178
- item = agents.get(slot)
179
- if not isinstance(item, dict):
180
- return None
181
- return AgentModelEndpoint.from_dict(item)
182
-
183
- def set_agent(self, slot: str, endpoint: AgentModelEndpoint | None) -> dict[str, object]:
184
- current = self.load()
185
- agents = current.get("agents", {})
186
- if not isinstance(agents, dict):
187
- agents = {}
188
- if endpoint is None:
189
- agents.pop(slot, None)
190
- else:
191
- agents[slot] = endpoint.to_dict()
192
- current["agents"] = agents
193
- current["updated_at"] = utc_now()
194
- self.path.write_text(json.dumps(current, indent=2, ensure_ascii=False), encoding="utf-8")
195
- return current
196
-
197
- def list_agents(self) -> dict[str, AgentModelEndpoint]:
198
- agents = self.load().get("agents", {})
199
- if not isinstance(agents, dict):
200
- return {}
201
- return {slot: AgentModelEndpoint.from_dict(item) for slot, item in agents.items() if isinstance(item, dict)}
167
+ def clear(self) -> dict[str, object]:
168
+ payload = self.save([])
169
+ payload["cleared"] = True
170
+ return payload
171
+
172
+ def get_router(self) -> AgentModelEndpoint | None:
173
+ router = self.load().get("router")
174
+ if not isinstance(router, dict):
175
+ return None
176
+ return AgentModelEndpoint.from_dict(router)
177
+
178
+ def set_router(self, endpoint: AgentModelEndpoint | None) -> dict[str, object]:
179
+ current = self.load()
180
+ current["updated_at"] = utc_now()
181
+ current["router"] = endpoint.to_dict() if endpoint else None
182
+ self.write_snapshot(current)
183
+ return current
184
+
185
+ def auto_route_enabled(self) -> bool:
186
+ return bool(self.load().get("auto_route", True))
187
+
188
+ def set_auto_route(self, enabled: bool) -> dict[str, object]:
189
+ current = self.load()
190
+ current["updated_at"] = utc_now()
191
+ current["auto_route"] = enabled
192
+ self.write_snapshot(current)
193
+ return current
194
+
195
+ def get_agent(self, slot: str) -> AgentModelEndpoint | None:
196
+ agents = self.load().get("agents", {})
197
+ if not isinstance(agents, dict):
198
+ return None
199
+ item = agents.get(slot)
200
+ if not isinstance(item, dict):
201
+ return None
202
+ return AgentModelEndpoint.from_dict(item)
203
+
204
+ def set_agent(self, slot: str, endpoint: AgentModelEndpoint | None) -> dict[str, object]:
205
+ current = self.load()
206
+ agents = current.get("agents", {})
207
+ if not isinstance(agents, dict):
208
+ agents = {}
209
+ if endpoint is None:
210
+ agents.pop(slot, None)
211
+ else:
212
+ agents[slot] = endpoint.to_dict()
213
+ current["agents"] = agents
214
+ current["updated_at"] = utc_now()
215
+ self.write_snapshot(current)
216
+ return current
217
+
218
+ def list_agents(self) -> dict[str, AgentModelEndpoint]:
219
+ agents = self.load().get("agents", {})
220
+ if not isinstance(agents, dict):
221
+ return {}
222
+ return {slot: AgentModelEndpoint.from_dict(item) for slot, item in agents.items() if isinstance(item, dict)}
202
223
 
203
224
 
204
225
  def ensure_ram_storage(root: str | Path) -> dict[str, object]:
@@ -208,7 +229,7 @@ def ensure_ram_storage(root: str | Path) -> dict[str, object]:
208
229
  events_path = ram_root / "events.jsonl"
209
230
  memory_path = ram_root / "memory.json"
210
231
  goal_path = ram_root / "goal_state.json"
211
- profile_path = ram_root / "agent_profile.json"
232
+ profile_path = ram_root / "agent_profile.jsonl"
212
233
 
213
234
  created: list[str] = []
214
235
  if not events_path.exists():
@@ -222,7 +243,7 @@ def ensure_ram_storage(root: str | Path) -> dict[str, object]:
222
243
  created.append("goal_state.json")
223
244
  if not profile_path.exists():
224
245
  JsonAgentProfileStore(profile_path).save([])
225
- created.append("agent_profile.json")
246
+ created.append("agent_profile.jsonl")
226
247
 
227
248
  return {
228
249
  "ram_root": str(ram_root),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentram",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
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",
@@ -50,3 +50,4 @@
50
50
  "access": "public"
51
51
  }
52
52
  }
53
+
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.29"
7
+ version = "0.1.32"
8
8
  description = "Async, model-agnostic RAM layer for agentic coding tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -39,3 +39,4 @@ agentram-daemon = "agentram.daemon:main"
39
39
  [tool.pytest.ini_options]
40
40
  testpaths = ["tests"]
41
41
  pythonpath = ["."]
42
+