agentram 0.1.29 → 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,13 +442,17 @@ 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
450
  self.messages: list[str] = []
479
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
480
456
  self.suggestions: list[str] = []
481
457
  self.setup_done = False
482
458
  self.prompt_history: list[str] = []
@@ -518,7 +494,6 @@ def run_textual_tui(context: McpContext) -> bool:
518
494
  with Horizontal(id="setup-actions"):
519
495
  yield Button("Back", id="setup-prev")
520
496
  yield Button("Next", id="setup-next", variant="primary")
521
- yield Button("Save setup", id="setup-save", variant="success")
522
497
  yield Button("Skip", id="setup-skip")
523
498
  yield RichLog(id="chat", wrap=True, highlight=True, markup=False, auto_scroll=True)
524
499
  yield Static(id="palette")
@@ -560,7 +535,7 @@ def run_textual_tui(context: McpContext) -> bool:
560
535
  self.query_one(f"#setup-{slot}-model", Input).value = endpoint.model
561
536
  self.query_one(f"#setup-{slot}-base-url", Input).value = endpoint.base_url
562
537
  self.query_one(f"#setup-{slot}-api-key-env", Input).value = endpoint.api_key_env
563
- 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.")
564
539
 
565
540
  def current_setup_slot(self) -> str:
566
541
  return self.setup_slots[self.setup_index]
@@ -580,12 +555,18 @@ def run_textual_tui(context: McpContext) -> bool:
580
555
  self.query_one(f"#setup-{self.current_setup_slot()}-model", Input).focus()
581
556
 
582
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}")
583
561
  if self.setup_index < len(self.setup_slots) - 1:
584
562
  self.setup_index += 1
585
563
  self.refresh_setup_step()
586
564
  self.focus_current_setup_model()
587
565
  else:
588
- 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()
589
570
 
590
571
  def action_prev_setup(self) -> None:
591
572
  if self.setup_index > 0:
@@ -602,34 +583,32 @@ def run_textual_tui(context: McpContext) -> bool:
602
583
  return self.setup_value(f"setup-{slot}-custom-provider") or "custom"
603
584
  return preset
604
585
 
605
- def action_save_setup(self) -> None:
586
+ def save_setup_slot(self, slot: str) -> str:
606
587
  role_by_slot = {"supervisor": "supervisor", "main": "coder", "small": "memory"}
607
- bound: list[str] = []
608
- for slot, role in role_by_slot.items():
609
- provider = self.setup_provider(slot)
610
- model = self.setup_value(f"setup-{slot}-model")
611
- base_url = self.setup_value(f"setup-{slot}-base-url")
612
- api_key_input = self.setup_value(f"setup-{slot}-api-key-env")
613
- api_key_env, saved_env_name = resolve_api_key_env(slot, api_key_input)
614
- if not model:
615
- continue
616
- endpoint = AgentModelEndpoint(
617
- provider=provider,
618
- model=model,
619
- role=role,
620
- base_url=base_url,
621
- api_key_env=api_key_env,
622
- modalities=["text"],
623
- )
624
- self.context.profile_store.set_agent(slot, endpoint)
625
- if saved_env_name:
626
- bound.append(f"{slot}={provider}:{model} key=.env:{saved_env_name}")
627
- else:
628
- bound.append(f"{slot}={provider}:{model}")
629
- if not bound:
630
- self.chat.write("agentram > setup skipped: no model entered")
631
- else:
632
- 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))
633
612
  self.hide_setup()
634
613
  self.refresh_sidebar()
635
614
  self.query_one("#input", Input).focus()
@@ -682,9 +661,7 @@ def run_textual_tui(context: McpContext) -> bool:
682
661
  self.last_ctrl_c_at = now
683
662
 
684
663
  def on_button_pressed(self, event: Button.Pressed) -> None:
685
- if event.button.id == "setup-save":
686
- self.action_save_setup()
687
- elif event.button.id == "setup-next":
664
+ if event.button.id == "setup-next":
688
665
  self.action_next_setup()
689
666
  elif event.button.id == "setup-prev":
690
667
  self.action_prev_setup()
@@ -761,6 +738,27 @@ def run_textual_tui(context: McpContext) -> bool:
761
738
  return
762
739
  if text in BUTTONS:
763
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
764
762
  if text and (not self.prompt_history or self.prompt_history[-1] != text):
765
763
  self.prompt_history.append(text)
766
764
  del self.prompt_history[:-100]
@@ -769,23 +767,49 @@ def run_textual_tui(context: McpContext) -> bool:
769
767
  self.chat.clear()
770
768
  self.chat.write("system > Chat cleared.")
771
769
  return
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:
772
790
  if self.busy:
773
- self.chat.write("agentram > busy, wait current task")
774
791
  return
775
792
  self.busy = True
776
- self.chat.write("agentram > thinking...")
777
793
  try:
778
- response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
779
- except Exception as error: # noqa: BLE001 - TUI boundary
780
- response = f"error: {error}"
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()
781
809
  finally:
782
810
  self.busy = False
783
- if response == "__clear__":
784
- self.chat.clear()
785
- self.chat.write("system > Chat cleared.")
786
- elif response:
787
- self.chat.write("agentram > " + response)
788
- self.refresh_sidebar()
811
+ self.processing_task = None
812
+ self.query_one("#input", Input).focus()
789
813
 
790
814
  AgentRAMTextualApp(context).run()
791
815
  return True
@@ -828,7 +852,7 @@ def create_prompt_session() -> Any | None:
828
852
  completer=completer,
829
853
  complete_while_typing=True,
830
854
  reserve_space_for_menu=12,
831
- 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",
832
856
  )
833
857
 
834
858
 
@@ -837,6 +861,35 @@ def read_tui_input(prompt_session: Any | None) -> str:
837
861
  return input("You > ")
838
862
  return prompt_session.prompt("You > ")
839
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
+
840
893
  def handle_chat_input(context: McpContext, text: str, messages: list[str] | None = None) -> str:
841
894
  if not text:
842
895
  return SLASH_HELP
@@ -1182,154 +1235,192 @@ def workflow_text(context: McpContext) -> str:
1182
1235
  lines.append(f"- {slot}: not bound")
1183
1236
  return "\n".join(lines)
1184
1237
 
1185
- def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
1186
- agents = context.profile_store.list_agents()
1187
- supervisor = agents.get("supervisor")
1188
- main_agent = agents.get("main")
1189
- small_agent = agents.get("small")
1190
- memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
1191
- route = classify_agent_runtime_route(context, prompt)
1192
- intent = str(route.get("intent", "chat"))
1193
- outputs: dict[str, str] = {}
1194
-
1195
- if intent == "planning":
1196
- plan = supervisor_plan(supervisor, prompt, memory_context)
1197
- outputs["supervisor"] = plan
1198
- outputs["small"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
1199
- sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1200
- persist_workflow_memory(context, prompt, plan, outputs)
1201
- return "\n".join(
1202
- [
1203
- f"Workflow: route=planning confidence={float(route.get('confidence', 0.0)):.2f} -> supervisor -> small memory note -> merge",
1204
- "Supervisor plan:",
1205
- plan,
1206
- "",
1207
- "Small memory agent notes:",
1208
- outputs.get("small", "No small agent bound."),
1209
- ]
1210
- )
1211
-
1212
- if intent == "memory":
1213
- plan = "Memory-only event. Small agent writes RAM note directly."
1214
- outputs["small"] = small_agent_observe(context, small_agent, prompt, "memory_event", prompt, 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=memory confidence={float(route.get('confidence', 0.0)):.2f} -> small memory note -> merge",
1220
- "Small memory agent notes:",
1221
- outputs.get("small", "No small agent bound."),
1222
- ]
1223
- )
1224
-
1225
- plan = runtime_execution_plan(context, supervisor, prompt, memory_context, route)
1226
- outputs["supervisor"] = plan
1227
- outputs["small_after_plan"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
1228
- if main_agent:
1229
- try:
1230
- outputs["main"] = call_agent_endpoint(main_agent, build_main_agent_prompt(prompt, plan, memory_context))
1231
- except Exception as error: # noqa: BLE001 - agent boundary
1232
- outputs["main"] = f"error: {model_error_hint(str(error))}"
1233
- else:
1234
- outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
1235
- outputs["small"] = small_agent_observe(context, small_agent, prompt, "main_result", outputs.get("main", ""), plan, memory_context)
1236
- sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1237
- persist_workflow_memory(context, prompt, plan, outputs)
1238
- return "\n".join(
1239
- [
1240
- f"Workflow: route={intent} confidence={float(route.get('confidence', 0.0)):.2f} -> main -> small memory note -> merge",
1241
- "Supervisor plan:",
1242
- plan,
1243
- "",
1244
- "Main agent result:",
1245
- outputs.get("main", "-"),
1246
- "",
1247
- "Small memory agent after plan:",
1248
- outputs.get("small_after_plan", "No small agent bound."),
1249
- "",
1250
- "Small memory agent notes:",
1251
- outputs.get("small", "No small agent bound."),
1252
- ]
1253
- )
1254
-
1255
- def classify_agent_runtime_route(context: McpContext, prompt: str) -> dict[str, object]:
1256
- lower = prompt.lower()
1257
- execute_words = [
1258
- "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",
1259
- "chốt kế hoạch", "chot ke hoach", "bắt tay", "start", "execute", "implement", "do it", "go ahead",
1260
- ]
1261
- plan_words = ["lên kế hoạch", "len ke hoach", "kế hoạch", "ke hoach", "plan", "roadmap", "milestone", "thiết kế", "thiet ke"]
1262
- if any(word in lower for word in execute_words):
1263
- return {"intent": "execution", "confidence": 0.9, "source": "heuristic"}
1264
- if any(word in lower for word in plan_words):
1265
- return {"intent": "planning", "confidence": 0.85, "source": "heuristic"}
1266
- decision = classify_prompt_intent(context, prompt)
1267
- intent = str(decision.get("intent", "chat"))
1268
- if intent == "coding":
1269
- decision["intent"] = "execution"
1270
- return decision
1271
-
1272
- def runtime_execution_plan(context: McpContext, supervisor: AgentModelEndpoint | None, prompt: str, memory_context: str, route: dict[str, object]) -> str:
1273
- state = context.goal_store.load().context_block()
1274
- if state:
1275
- return "\n".join(["Use current accepted Goal Stack as execution plan.", state])
1276
- 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"]):
1277
- return "User approved previous plan. Use AgentRAM memory context and current prompt to execute next concrete task."
1278
- return supervisor_plan(supervisor, prompt, memory_context)
1279
-
1280
- def small_agent_observe(context: McpContext, small_agent: AgentModelEndpoint | None, prompt: str, event_type: str, event_content: str, plan: str, memory_context: str) -> str:
1281
- if not small_agent:
1282
- return "No small agent bound."
1283
- try:
1284
- return call_agent_endpoint(
1285
- small_agent,
1286
- build_small_agent_prompt(prompt, plan, memory_context, event_content, event_type=event_type),
1287
- )
1288
- except Exception as error: # noqa: BLE001 - agent boundary
1289
- return f"error: {model_error_hint(str(error))}"
1290
-
1291
- def sync_goal_state_from_small_agent(context: McpContext, prompt: str, plan: str, small_notes: str) -> None:
1292
- if not small_notes.strip() or small_notes.strip().startswith("error:"):
1293
- return
1294
- state = context.goal_store.load()
1295
- goal = extract_labeled_value(small_notes, ("current_goal", "goal", "mục tiêu", "muc tieu")) or first_nonempty_line(plan)
1296
- task = extract_labeled_value(small_notes, ("current_task", "task", "nhiệm vụ", "nhiem vu")) or first_nonempty_line(small_notes) or prompt
1297
- patch: dict[str, object] = {
1298
- "current_task": trim(task.strip(), 180),
1299
- "goal_history": [trim(goal.strip() or prompt.strip(), 180)],
1300
- }
1301
- if not state.mission:
1302
- patch["mission"] = "Xây dựng vận hành AgentRAM cho coding agents."
1303
- if not state.project_goal:
1304
- patch["project_goal"] = "AgentRAM workflow: supervisor lập kế hoạch, main coding agent thực thi, small memory agent ghi RAM."
1305
- if goal:
1306
- patch["current_goal"] = trim(goal.strip(), 180)
1307
- context.goal_store.update(patch)
1308
-
1309
- def extract_labeled_value(text: str, labels: tuple[str, ...]) -> str:
1310
- for line in text.splitlines():
1311
- cleaned = line.strip().lstrip("-*").strip()
1312
- for label in labels:
1313
- pattern = rf"^{re.escape(label)}\s*[:=]\s*(.+)$"
1314
- match = re.match(pattern, cleaned, flags=re.IGNORECASE)
1315
- if match:
1316
- return match.group(1).strip().strip("`*_-")
1317
- return ""
1318
-
1319
- def first_nonempty_line(value: str) -> str:
1320
- for line in value.splitlines():
1321
- cleaned = line.strip().lstrip("-0123456789. )")
1322
- if cleaned and not cleaned.lower().startswith(("supervisor error", "fallback plan")):
1323
- return cleaned
1324
- return ""
1325
-
1326
- def persist_workflow_memory(context: McpContext, prompt: str, plan: str, outputs: dict[str, str]) -> None:
1327
- small_notes = "\n".join(
1328
- value.strip()
1329
- for key, value in outputs.items()
1330
- if key.startswith("small") and value.strip() and not value.strip().startswith("error:")
1331
- )
1332
- 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()
1333
1424
  note_parts = [
1334
1425
  f"User request: {prompt}",
1335
1426
  f"Supervisor plan: {trim(plan.strip(), 1200)}" if plan.strip() else "Supervisor plan: -",
@@ -1360,14 +1451,14 @@ def supervisor_plan(supervisor: AgentModelEndpoint | None, prompt: str, memory_c
1360
1451
  def call_agent_endpoint(endpoint: AgentModelEndpoint, prompt: str) -> str:
1361
1452
  return MultiModelCodingOrchestrator([endpoint])._call_endpoint(endpoint, prompt)
1362
1453
 
1363
- def build_supervisor_prompt(prompt: str, memory_context: str) -> str:
1364
- 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])
1365
-
1366
- def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
1367
- 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])
1368
-
1369
- def build_small_agent_prompt(prompt: str, plan: str, memory_context: str, event_content: str = "", event_type: str = "main_result") -> str:
1370
- 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])
1371
1462
 
1372
1463
  def bind_router_text(context: McpContext, options: dict[str, object]) -> str:
1373
1464
  api_key_env = str(options.get("api_key_env", ""))
@@ -1498,7 +1589,7 @@ def model_error_hint(message: str) -> str:
1498
1589
  if "returned non-json" in lower or "expecting value" in lower:
1499
1590
  return message + " | Fix: endpoint returned empty/HTML/non-JSON. Check base_url, auth key, proxy, and server logs."
1500
1591
  if "command not found" in lower or "winerror 2" in lower or "the system cannot find the file" in lower:
1501
- 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."
1502
1593
  if "missing api key env" in lower:
1503
1594
  return message + " | Fix: set environment variable, then bind using --api-key-env ENV_NAME, not raw key."
1504
1595
  return message
@@ -1611,20 +1702,14 @@ def sidebar_lines(context: McpContext) -> list[str]:
1611
1702
  "TASK",
1612
1703
  f" {trim(goal.current_task or '-', 24)}",
1613
1704
  "",
1614
- "BUTTONS",
1615
- " 1 init 2 status",
1616
- " 3 retrieve 4 goal",
1617
- " 5 set-goal 6 drift",
1618
- " 7 events 8 replay",
1619
- " 9 help 0 exit",
1620
- "",
1621
- "CLAUDE",
1705
+ "COMMANDS",
1622
1706
  " / = command palette",
1623
- " /agents",
1624
- " /claude-install .",
1625
- " /models",
1626
- " /clear-models",
1627
- " /ask <prompt>",
1707
+ " /setup",
1708
+ " /status",
1709
+ " /goal",
1710
+ " /workflow",
1711
+ " /note <text>",
1712
+ " /clear",
1628
1713
  ]
1629
1714
 
1630
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.29",
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
+