agentram 0.1.30 → 0.1.34

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
 
@@ -467,19 +439,24 @@ def run_textual_tui(context: McpContext) -> bool:
467
439
  .setup-input { height: 3; }
468
440
  #chat { height: 1fr; border: solid #263241; padding: 1; overflow-y: scroll; }
469
441
  #palette { height: auto; max-height: 12; border: solid #3b82f6; padding: 1; background: #101826; display: none; }
442
+ #approval { height: auto; border: heavy #f59e0b; padding: 1; background: #1c1608; display: none; }
443
+ #approval-title { color: #fde68a; }
444
+ #approval-actions { height: auto; }
470
445
  #input { dock: bottom; border: solid #3b82f6; }
471
446
  .muted { color: #8b9bb0; }
472
447
  """
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")]
448
+ BINDINGS = [("ctrl+c", "interrupt_or_quit", "Cancel/Quit"), ("tab", "accept_suggestion", "Complete slash"), ("up", "history_prev", "Previous prompt"), ("down", "history_next", "Next prompt")]
474
449
 
475
450
  def __init__(self, mcp_context: McpContext) -> None:
476
451
  super().__init__()
477
452
  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] = []
453
+ self.messages: list[str] = []
454
+ self.busy = False
455
+ self.prompt_queue: list[str] = []
456
+ self.processing_task: asyncio.Task[None] | None = None
457
+ self.pending_approval_prompt: str | None = None
458
+ self.project_main_agent_approved = False
459
+ self.suggestions: list[str] = []
483
460
  self.setup_done = False
484
461
  self.prompt_history: list[str] = []
485
462
  self.history_index: int | None = None
@@ -520,9 +497,15 @@ def run_textual_tui(context: McpContext) -> bool:
520
497
  with Horizontal(id="setup-actions"):
521
498
  yield Button("Back", id="setup-prev")
522
499
  yield Button("Next", id="setup-next", variant="primary")
523
- yield Button("Save setup", id="setup-save", variant="success")
524
500
  yield Button("Skip", id="setup-skip")
525
501
  yield RichLog(id="chat", wrap=True, highlight=True, markup=False, auto_scroll=True)
502
+ with Vertical(id="approval"):
503
+ yield Static("Main agent wants terminal/file access. Allow?", id="approval-title")
504
+ yield Static("This can run commands or edit files in the current project.")
505
+ with Horizontal(id="approval-actions"):
506
+ yield Button("Yes", id="approval-yes", variant="success")
507
+ yield Button("Yes, do this project", id="approval-project", variant="primary")
508
+ yield Button("No", id="approval-no", variant="error")
526
509
  yield Static(id="palette")
527
510
  yield Input(placeholder="Prompt or /command. Mouse wheel scroll in chat.", id="input")
528
511
  yield Footer()
@@ -562,7 +545,7 @@ def run_textual_tui(context: McpContext) -> bool:
562
545
  self.query_one(f"#setup-{slot}-model", Input).value = endpoint.model
563
546
  self.query_one(f"#setup-{slot}-base-url", Input).value = endpoint.base_url
564
547
  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.")
548
+ self.chat.write("system > Existing workflow models loaded. Next auto-saves each step, or Skip to keep.")
566
549
 
567
550
  def current_setup_slot(self) -> str:
568
551
  return self.setup_slots[self.setup_index]
@@ -582,12 +565,18 @@ def run_textual_tui(context: McpContext) -> bool:
582
565
  self.query_one(f"#setup-{self.current_setup_slot()}-model", Input).focus()
583
566
 
584
567
  def action_next_setup(self) -> None:
568
+ saved = self.save_setup_slot(self.current_setup_slot())
569
+ if saved:
570
+ self.chat.write(f"agentram > setup saved: {saved}")
585
571
  if self.setup_index < len(self.setup_slots) - 1:
586
572
  self.setup_index += 1
587
573
  self.refresh_setup_step()
588
574
  self.focus_current_setup_model()
589
575
  else:
590
- self.action_save_setup()
576
+ self.chat.write("agentram > setup complete")
577
+ self.hide_setup()
578
+ self.refresh_sidebar()
579
+ self.query_one("#input", Input).focus()
591
580
 
592
581
  def action_prev_setup(self) -> None:
593
582
  if self.setup_index > 0:
@@ -604,34 +593,32 @@ def run_textual_tui(context: McpContext) -> bool:
604
593
  return self.setup_value(f"setup-{slot}-custom-provider") or "custom"
605
594
  return preset
606
595
 
607
- def action_save_setup(self) -> None:
596
+ def save_setup_slot(self, slot: str) -> str:
608
597
  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))
598
+ role = role_by_slot[slot]
599
+ provider = self.setup_provider(slot)
600
+ model = self.setup_value(f"setup-{slot}-model")
601
+ base_url = self.setup_value(f"setup-{slot}-base-url")
602
+ api_key_input = self.setup_value(f"setup-{slot}-api-key-env")
603
+ api_key_env, saved_env_name = resolve_api_key_env(slot, api_key_input)
604
+ if not model:
605
+ return f"{slot}=skipped"
606
+ endpoint = AgentModelEndpoint(
607
+ provider=provider,
608
+ model=model,
609
+ role=role,
610
+ base_url=base_url,
611
+ api_key_env=api_key_env,
612
+ modalities=["text"],
613
+ )
614
+ self.context.profile_store.set_agent(slot, endpoint)
615
+ if saved_env_name:
616
+ return f"{slot}={provider}:{model} key=.env:{saved_env_name}"
617
+ return f"{slot}={provider}:{model}"
618
+
619
+ def action_save_setup(self) -> None:
620
+ bound = [self.save_setup_slot(slot) for slot in self.setup_slots]
621
+ self.chat.write("agentram > setup saved: " + ", ".join(bound))
635
622
  self.hide_setup()
636
623
  self.refresh_sidebar()
637
624
  self.query_one("#input", Input).focus()
@@ -684,8 +671,12 @@ def run_textual_tui(context: McpContext) -> bool:
684
671
  self.last_ctrl_c_at = now
685
672
 
686
673
  def on_button_pressed(self, event: Button.Pressed) -> None:
687
- if event.button.id == "setup-save":
688
- self.action_save_setup()
674
+ if event.button.id == "approval-yes":
675
+ self.approve_pending_prompt("yes_once")
676
+ elif event.button.id == "approval-project":
677
+ self.approve_pending_prompt("yes_project")
678
+ elif event.button.id == "approval-no":
679
+ self.approve_pending_prompt("no")
689
680
  elif event.button.id == "setup-next":
690
681
  self.action_next_setup()
691
682
  elif event.button.id == "setup-prev":
@@ -695,6 +686,36 @@ def run_textual_tui(context: McpContext) -> bool:
695
686
  self.hide_setup()
696
687
  self.query_one("#input", Input).focus()
697
688
 
689
+ def show_approval_popup(self, prompt: str) -> None:
690
+ panel = self.query_one("#approval", Vertical)
691
+ self.query_one("#approval-title", Static).update("Main agent wants terminal/file access. Allow?")
692
+ panel.styles.display = "block"
693
+ self.chat.write("agentram > approval needed: click Yes / Yes, do this project / No")
694
+
695
+ def hide_approval_popup(self) -> None:
696
+ try:
697
+ self.query_one("#approval", Vertical).styles.display = "none"
698
+ except Exception: # noqa: BLE001 - widget may not be mounted yet
699
+ return
700
+
701
+ def approve_pending_prompt(self, decision: str) -> None:
702
+ if self.pending_approval_prompt is None:
703
+ self.hide_approval_popup()
704
+ return
705
+ approved = self.pending_approval_prompt
706
+ self.pending_approval_prompt = None
707
+ self.hide_approval_popup()
708
+ if decision == "yes_project":
709
+ self.project_main_agent_approved = True
710
+ self.chat.write("agentram > approved for this project")
711
+ self.enqueue_prompt(approved)
712
+ return
713
+ if decision == "yes_once":
714
+ self.chat.write("agentram > approved once")
715
+ self.enqueue_prompt(approved)
716
+ return
717
+ self.chat.write("agentram > denied; prompt dropped")
718
+
698
719
  def hide_palette(self) -> None:
699
720
  try:
700
721
  palette = self.query_one("#palette", Static)
@@ -763,6 +784,30 @@ def run_textual_tui(context: McpContext) -> bool:
763
784
  return
764
785
  if text in BUTTONS:
765
786
  text = BUTTONS[text]
787
+ if self.pending_approval_prompt is not None:
788
+ decision = normalize_approval_decision(text)
789
+ if decision == "yes_once":
790
+ approved = self.pending_approval_prompt
791
+ self.pending_approval_prompt = None
792
+ self.hide_approval_popup()
793
+ self.chat.write("agentram > approved once")
794
+ self.enqueue_prompt(approved)
795
+ return
796
+ if decision == "yes_project":
797
+ approved = self.pending_approval_prompt
798
+ self.pending_approval_prompt = None
799
+ self.hide_approval_popup()
800
+ self.project_main_agent_approved = True
801
+ self.chat.write("agentram > approved for this project")
802
+ self.enqueue_prompt(approved)
803
+ return
804
+ if decision == "no":
805
+ self.chat.write("agentram > denied; prompt dropped")
806
+ self.pending_approval_prompt = None
807
+ self.hide_approval_popup()
808
+ return
809
+ self.chat.write("agentram > approval required: click Yes / Yes, do this project / No")
810
+ return
766
811
  if text and (not self.prompt_history or self.prompt_history[-1] != text):
767
812
  self.prompt_history.append(text)
768
813
  del self.prompt_history[:-100]
@@ -771,37 +816,44 @@ def run_textual_tui(context: McpContext) -> bool:
771
816
  self.chat.clear()
772
817
  self.chat.write("system > Chat cleared.")
773
818
  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()
819
+ if requires_main_agent_approval(self.context, text) and not self.project_main_agent_approved:
820
+ self.pending_approval_prompt = text
821
+ self.show_approval_popup(text)
822
+ return
823
+ self.enqueue_prompt(text)
824
+
825
+ def enqueue_prompt(self, text: str) -> None:
826
+ if self.busy:
827
+ self.prompt_queue.append(text)
828
+ self.chat.write(f"agentram > queued #{len(self.prompt_queue)}")
829
+ return
830
+ self.prompt_queue.append(text)
831
+ self.processing_task = asyncio.create_task(self.process_prompt_queue())
832
+
833
+ async def process_prompt_queue(self) -> None:
834
+ if self.busy:
835
+ return
836
+ self.busy = True
837
+ try:
838
+ while self.prompt_queue:
839
+ text = self.prompt_queue.pop(0)
840
+ pending = len(self.prompt_queue)
841
+ suffix = f" ({pending} queued)" if pending else ""
842
+ self.chat.write(f"agentram > thinking...{suffix}")
843
+ try:
844
+ response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
845
+ except Exception as error: # noqa: BLE001 - TUI boundary
846
+ response = f"error: {error}"
847
+ if response == "__clear__":
848
+ self.chat.clear()
849
+ self.chat.write("system > Chat cleared.")
850
+ elif response:
851
+ self.chat.write("agentram > " + response)
852
+ self.refresh_sidebar()
853
+ finally:
854
+ self.busy = False
855
+ self.processing_task = None
856
+ self.query_one("#input", Input).focus()
805
857
 
806
858
  AgentRAMTextualApp(context).run()
807
859
  return True
@@ -844,7 +896,7 @@ def create_prompt_session() -> Any | None:
844
896
  completer=completer,
845
897
  complete_while_typing=True,
846
898
  reserve_space_for_menu=12,
847
- bottom_toolbar="Type / for slash commands | /agents for subagents | /ask to call bound models",
899
+ bottom_toolbar="Type / for AgentRAM commands | plain text routes through supervisor/main/small",
848
900
  )
849
901
 
850
902
 
@@ -853,6 +905,35 @@ def read_tui_input(prompt_session: Any | None) -> str:
853
905
  return input("You > ")
854
906
  return prompt_session.prompt("You > ")
855
907
 
908
+ def normalize_approval_decision(text: str) -> str:
909
+ normalized = text.strip().lower()
910
+ if normalized in {"yes", "y", "ok", "allow", "approve", "đồng ý", "dong y"}:
911
+ return "yes_once"
912
+ if normalized in {"yes do this project", "yes project", "always", "a", "p", "allow project", "approve project", "đồng ý project", "dong y project"}:
913
+ return "yes_project"
914
+ if normalized in {"no", "n", "deny", "reject", "không", "khong"}:
915
+ return "no"
916
+ return ""
917
+
918
+ def requires_main_agent_approval(context: McpContext, prompt: str) -> bool:
919
+ main_agent = context.profile_store.list_agents().get("main")
920
+ if not main_agent:
921
+ return False
922
+ provider = main_agent.provider.lower()
923
+ if provider not in {"codex-cli", "claude-code", "claude-cli", "claude-subagent", "claude-code-subagent"}:
924
+ return False
925
+ route = heuristic_agent_runtime_route(prompt)
926
+ if str(route.get("intent")) == "planning":
927
+ return False
928
+ if str(route.get("intent")) == "execution":
929
+ return True
930
+ lower = prompt.lower()
931
+ risky_words = [
932
+ "terminal", "command", "shell", "powershell", "cmd", "npm", "pip", "git", "pytest", "run", "build",
933
+ "sửa", "sua", "fix", "refactor", "edit", "write", "delete", "xóa", "xoa", "tạo file", "tao file",
934
+ ]
935
+ return any(word in lower for word in risky_words)
936
+
856
937
  def handle_chat_input(context: McpContext, text: str, messages: list[str] | None = None) -> str:
857
938
  if not text:
858
939
  return SLASH_HELP
@@ -1198,192 +1279,285 @@ def workflow_text(context: McpContext) -> str:
1198
1279
  lines.append(f"- {slot}: not bound")
1199
1280
  return "\n".join(lines)
1200
1281
 
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()
1282
+ def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
1283
+ agents = context.profile_store.list_agents()
1284
+ supervisor = agents.get("supervisor")
1285
+ main_agent = agents.get("main")
1286
+ small_agent = agents.get("small")
1287
+ memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
1288
+ route = classify_agent_runtime_route(context, prompt, main_agent)
1289
+ intent = str(route.get("intent", "chat"))
1290
+ outputs: dict[str, str] = {}
1291
+
1292
+ if intent == "planning":
1293
+ plan = supervisor_plan(supervisor, prompt, memory_context)
1294
+ outputs["supervisor"] = plan
1295
+ outputs["small"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
1296
+ sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1297
+ persist_workflow_memory(context, prompt, plan, outputs)
1298
+ return "\n".join(
1299
+ [
1300
+ f"Workflow: route=planning confidence={float(route.get('confidence', 0.0)):.2f} -> supervisor -> small memory note -> merge",
1301
+ "Supervisor plan:",
1302
+ plan,
1303
+ "",
1304
+ "Small memory agent notes:",
1305
+ outputs.get("small", "No small agent bound."),
1306
+ ]
1307
+ )
1308
+
1309
+ if intent == "memory":
1310
+ plan = "Memory-only event. Small agent writes RAM note directly."
1311
+ outputs["small"] = small_agent_observe(context, small_agent, prompt, "memory_event", prompt, plan, memory_context)
1312
+ sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1313
+ persist_workflow_memory(context, prompt, plan, outputs)
1314
+ return "\n".join(
1315
+ [
1316
+ f"Workflow: route=memory confidence={float(route.get('confidence', 0.0)):.2f} -> small memory note -> merge",
1317
+ "Small memory agent notes:",
1318
+ outputs.get("small", "No small agent bound."),
1319
+ ]
1320
+ )
1321
+
1322
+ plan = runtime_execution_plan(context, supervisor, prompt, memory_context, route)
1323
+ outputs["supervisor"] = plan
1324
+ outputs["small_after_plan"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
1325
+ loop_result = run_main_agent_loop(context, main_agent, small_agent, prompt, plan, memory_context)
1326
+ outputs.update(loop_result["outputs"])
1327
+ sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1328
+ persist_workflow_memory(context, prompt, plan, outputs)
1329
+ return "\n".join(
1330
+ [
1331
+ f"Workflow: route={intent} confidence={float(route.get('confidence', 0.0)):.2f} -> main loop -> small memory note -> merge",
1332
+ f"Loop: steps={loop_result['steps']} status={loop_result['status']} retries={loop_result['retries']}",
1333
+ "Supervisor plan:",
1334
+ plan,
1335
+ "",
1336
+ "Main agent result:",
1337
+ outputs.get("main", "-"),
1338
+ "",
1339
+ "Small memory agent after plan:",
1340
+ outputs.get("small_after_plan", "No small agent bound."),
1341
+ "",
1342
+ "Small memory agent notes:",
1343
+ outputs.get("small", "No small agent bound."),
1344
+ ]
1345
+ )
1346
+
1347
+ def main_loop_limits() -> tuple[int, int]:
1348
+ max_steps = max(1, min(10, int(os.getenv("AGENTRAM_MAIN_MAX_STEPS", "3"))))
1349
+ max_retries = max(0, min(5, int(os.getenv("AGENTRAM_MAIN_MAX_RETRIES", "1"))))
1350
+ return max_steps, max_retries
1351
+
1352
+ def run_main_agent_loop(
1353
+ context: McpContext,
1354
+ main_agent: AgentModelEndpoint | None,
1355
+ small_agent: AgentModelEndpoint | None,
1356
+ prompt: str,
1357
+ plan: str,
1358
+ memory_context: str,
1359
+ ) -> dict[str, object]:
1360
+ outputs: dict[str, str] = {}
1361
+ if not main_agent:
1362
+ outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
1363
+ outputs["small"] = small_agent_observe(context, small_agent, prompt, "main_result", outputs["main"], plan, memory_context)
1364
+ return {"outputs": outputs, "steps": 0, "status": "no_main", "retries": 0}
1365
+
1366
+ max_steps, max_retries = main_loop_limits()
1367
+ retries = 0
1368
+ status = "max_steps"
1369
+ previous_main_outputs: list[str] = []
1370
+ small_notes: list[str] = []
1371
+
1372
+ for step in range(1, max_steps + 1):
1373
+ step_prompt = build_main_loop_prompt(prompt, plan, memory_context, previous_main_outputs, small_notes, step, max_steps)
1374
+ try:
1375
+ main_output = call_agent_endpoint(main_agent, step_prompt)
1376
+ except Exception as error: # noqa: BLE001 - agent boundary
1377
+ main_output = f"error: {model_error_hint(str(error))}"
1378
+ retries += 1
1379
+ previous_main_outputs.append(f"Step {step}: {main_output}")
1380
+ small_note = small_agent_observe(context, small_agent, prompt, f"main_error_step_{step}", main_output, plan, memory_context)
1381
+ small_notes.append(small_note)
1382
+ outputs[f"main_step_{step}"] = main_output
1383
+ outputs[f"small_step_{step}"] = small_note
1384
+ if retries > max_retries:
1385
+ status = "error_retry_limit"
1386
+ break
1387
+ continue
1388
+
1389
+ previous_main_outputs.append(f"Step {step}: {main_output}")
1390
+ small_note = small_agent_observe(context, small_agent, prompt, f"main_result_step_{step}", main_output, plan, memory_context)
1391
+ small_notes.append(small_note)
1392
+ outputs[f"main_step_{step}"] = main_output
1393
+ outputs[f"small_step_{step}"] = small_note
1394
+ decision = small_loop_decision(main_output, small_note)
1395
+ status = decision
1396
+ if decision in {"done", "blocker", "error"}:
1397
+ break
1398
+
1399
+ outputs["main"] = "\n\n".join(previous_main_outputs).strip() or "-"
1400
+ outputs["small"] = "\n\n".join(small_notes).strip() or "No small agent bound."
1401
+ return {"outputs": outputs, "steps": len(previous_main_outputs), "status": status, "retries": retries}
1402
+
1403
+ def build_main_loop_prompt(
1404
+ prompt: str,
1405
+ plan: str,
1406
+ memory_context: str,
1407
+ previous_main_outputs: list[str],
1408
+ small_notes: list[str],
1409
+ step: int,
1410
+ max_steps: int,
1411
+ ) -> str:
1412
+ history = "\n\n".join([*previous_main_outputs[-3:], *small_notes[-3:]]) or "-"
1413
+ return "\n".join(
1414
+ [
1415
+ f"AgentRAM main loop step {step}/{max_steps}.",
1416
+ "Continue only if needed. Stop when done or blocked.",
1417
+ "If complete, include DONE. If blocked, include BLOCKER and reason.",
1418
+ "User request:",
1419
+ prompt,
1420
+ "Supervisor / execution plan:",
1421
+ plan,
1422
+ "Recent main/small feedback:",
1423
+ history,
1424
+ "AgentRAM context:",
1425
+ memory_context,
1426
+ ]
1427
+ )
1428
+
1429
+ def small_loop_decision(main_output: str, small_note: str) -> str:
1430
+ combined = f"{main_output}\n{small_note}".lower()
1431
+ if "error:" in main_output.lower():
1432
+ return "error"
1433
+ done_markers = ["loop_decision: done", "status: done", "done", "hoàn thành", "hoan thanh", "complete", "completed"]
1434
+ blocker_markers = ["loop_decision: blocker", "status: blocker", "blocker", "blocked", "bị chặn", "bi chan", "không thể tiếp tục", "khong the tiep tuc"]
1435
+ continue_markers = ["loop_decision: continue", "status: continue", "continue", "tiếp tục", "tiep tuc", "next step"]
1436
+ if any(marker in combined for marker in blocker_markers):
1437
+ return "blocker"
1438
+ if any(marker in combined for marker in continue_markers):
1439
+ return "continue"
1440
+ if any(marker in combined for marker in done_markers):
1441
+ return "done"
1442
+ return "done"
1443
+
1444
+
1445
+ def classify_agent_runtime_route(context: McpContext, prompt: str, main_agent: AgentModelEndpoint | None = None) -> dict[str, object]:
1446
+ if main_agent:
1447
+ try:
1448
+ response = call_agent_endpoint(main_agent, build_agent_runtime_router_prompt(prompt))
1449
+ data = json.loads(extract_json_object(response))
1450
+ intent = str(data.get("intent", "")).strip().lower()
1451
+ if intent in {"planning", "execution", "memory", "chat"}:
1452
+ return {
1453
+ "intent": intent,
1454
+ "confidence": float(data.get("confidence", 0.75)),
1455
+ "reason": str(data.get("reason", "main-agent router")),
1456
+ "source": "main-agent",
1457
+ }
1458
+ except Exception as error: # noqa: BLE001 - router boundary
1459
+ fallback = heuristic_agent_runtime_route(prompt)
1460
+ fallback["source"] = "heuristic"
1461
+ fallback["router_error"] = str(error)
1462
+ return fallback
1463
+
1464
+ return heuristic_agent_runtime_route(prompt)
1465
+
1466
+ def heuristic_agent_runtime_route(prompt: str) -> dict[str, object]:
1467
+ lower = prompt.lower()
1468
+ execute_words = [
1469
+ "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",
1470
+ "chốt kế hoạch", "chot ke hoach", "bắt tay", "start", "execute", "implement", "do it", "go ahead",
1471
+ ]
1472
+ plan_words = ["lên kế hoạch", "len ke hoach", "kế hoạch", "ke hoach", "plan", "roadmap", "milestone", "thiết kế", "thiet ke"]
1473
+ if any(word in lower for word in execute_words):
1474
+ return {"intent": "execution", "confidence": 0.9, "source": "heuristic"}
1475
+ if any(word in lower for word in plan_words):
1476
+ return {"intent": "planning", "confidence": 0.85, "source": "heuristic"}
1477
+ decision = heuristic_intent(prompt)
1478
+ intent = str(decision.get("intent", "chat"))
1479
+ if intent == "coding":
1480
+ decision["intent"] = "execution"
1481
+ decision["source"] = "heuristic"
1482
+ return decision
1483
+
1484
+ def build_agent_runtime_router_prompt(prompt: str) -> str:
1485
+ return "\n".join(
1486
+ [
1487
+ "You are AgentRAM orchestration router using the main agent model.",
1488
+ "Classify which agent should handle the user prompt. Do not edit files. Do not run tools. Return only JSON.",
1489
+ "Schema: {\"intent\":\"planning|execution|memory|chat\",\"confidence\":0.0,\"reason\":\"short\"}",
1490
+ "planning = user asks to plan, design, set roadmap, discuss goal, no execution yet.",
1491
+ "execution = user approves plan, says start/do it, or asks coding/refactor/fix/test/file work.",
1492
+ "memory = user asks to remember/save/note a decision/fact.",
1493
+ "chat = normal conversation.",
1494
+ "Nếu user prompt bằng tiếng Việt, reason dùng tiếng Việt.",
1495
+ "User prompt:",
1496
+ prompt,
1497
+ ]
1498
+ )
1499
+
1500
+ def runtime_execution_plan(context: McpContext, supervisor: AgentModelEndpoint | None, prompt: str, memory_context: str, route: dict[str, object]) -> str:
1501
+ state = context.goal_store.load().context_block()
1502
+ if state:
1503
+ return "\n".join(["Use current accepted Goal Stack as execution plan.", state])
1504
+ 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"]):
1505
+ return "User approved previous plan. Use AgentRAM memory context and current prompt to execute next concrete task."
1506
+ return supervisor_plan(supervisor, prompt, memory_context)
1507
+
1508
+ def small_agent_observe(context: McpContext, small_agent: AgentModelEndpoint | None, prompt: str, event_type: str, event_content: str, plan: str, memory_context: str) -> str:
1509
+ if not small_agent:
1510
+ return "No small agent bound."
1511
+ try:
1512
+ return call_agent_endpoint(
1513
+ small_agent,
1514
+ build_small_agent_prompt(prompt, plan, memory_context, event_content, event_type=event_type),
1515
+ )
1516
+ except Exception as error: # noqa: BLE001 - agent boundary
1517
+ return f"error: {model_error_hint(str(error))}"
1518
+
1519
+ def sync_goal_state_from_small_agent(context: McpContext, prompt: str, plan: str, small_notes: str) -> None:
1520
+ if not small_notes.strip() or small_notes.strip().startswith("error:"):
1521
+ return
1522
+ state = context.goal_store.load()
1523
+ goal = extract_labeled_value(small_notes, ("current_goal", "goal", "mục tiêu", "muc tieu")) or first_nonempty_line(plan)
1524
+ task = extract_labeled_value(small_notes, ("current_task", "task", "nhiệm vụ", "nhiem vu")) or first_nonempty_line(small_notes) or prompt
1525
+ patch: dict[str, object] = {
1526
+ "current_task": trim(task.strip(), 180),
1527
+ "goal_history": [trim(goal.strip() or prompt.strip(), 180)],
1528
+ }
1529
+ if not state.mission:
1530
+ patch["mission"] = "Xây dựng và vận hành AgentRAM cho coding agents."
1531
+ if not state.project_goal:
1532
+ patch["project_goal"] = "AgentRAM workflow: supervisor lập kế hoạch, main coding agent thực thi, small memory agent ghi RAM."
1533
+ if goal:
1534
+ patch["current_goal"] = trim(goal.strip(), 180)
1535
+ context.goal_store.update(patch)
1536
+
1537
+ def extract_labeled_value(text: str, labels: tuple[str, ...]) -> str:
1538
+ for line in text.splitlines():
1539
+ cleaned = line.strip().lstrip("-*").strip()
1540
+ for label in labels:
1541
+ pattern = rf"^{re.escape(label)}\s*[:=]\s*(.+)$"
1542
+ match = re.match(pattern, cleaned, flags=re.IGNORECASE)
1543
+ if match:
1544
+ return match.group(1).strip().strip("`*_-")
1545
+ return ""
1546
+
1547
+ def first_nonempty_line(value: str) -> str:
1548
+ for line in value.splitlines():
1549
+ cleaned = line.strip().lstrip("-0123456789. )")
1550
+ if cleaned and not cleaned.lower().startswith(("supervisor error", "fallback plan")):
1551
+ return cleaned
1552
+ return ""
1553
+
1554
+ def persist_workflow_memory(context: McpContext, prompt: str, plan: str, outputs: dict[str, str]) -> None:
1555
+ small_notes = "\n".join(
1556
+ value.strip()
1557
+ for key, value in outputs.items()
1558
+ if key.startswith("small") and value.strip() and not value.strip().startswith("error:")
1559
+ )
1560
+ main_result = outputs.get("main", "").strip()
1387
1561
  note_parts = [
1388
1562
  f"User request: {prompt}",
1389
1563
  f"Supervisor plan: {trim(plan.strip(), 1200)}" if plan.strip() else "Supervisor plan: -",
@@ -1414,14 +1588,14 @@ def supervisor_plan(supervisor: AgentModelEndpoint | None, prompt: str, memory_c
1414
1588
  def call_agent_endpoint(endpoint: AgentModelEndpoint, prompt: str) -> str:
1415
1589
  return MultiModelCodingOrchestrator([endpoint])._call_endpoint(endpoint, prompt)
1416
1590
 
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])
1591
+ def build_supervisor_prompt(prompt: str, memory_context: str) -> str:
1592
+ 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])
1593
+
1594
+ def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
1595
+ 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])
1596
+
1597
+ def build_small_agent_prompt(prompt: str, plan: str, memory_context: str, event_content: str = "", event_type: str = "main_result") -> str:
1598
+ 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: ...", "Nếu main cần chạy tiếp, thêm dòng loop_decision: continue. Nếu xong, loop_decision: done. Nếu bị chặn, loop_decision: blocker.", "User request:", prompt, "Supervisor plan:", plan, "Observed event type:", event_type, "Observed event content:", event_content or "-", "AgentRAM context:", memory_context])
1425
1599
 
1426
1600
  def bind_router_text(context: McpContext, options: dict[str, object]) -> str:
1427
1601
  api_key_env = str(options.get("api_key_env", ""))
@@ -1552,7 +1726,7 @@ def model_error_hint(message: str) -> str:
1552
1726
  if "returned non-json" in lower or "expecting value" in lower:
1553
1727
  return message + " | Fix: endpoint returned empty/HTML/non-JSON. Check base_url, auth key, proxy, and server logs."
1554
1728
  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."
1729
+ 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
1730
  if "missing api key env" in lower:
1557
1731
  return message + " | Fix: set environment variable, then bind using --api-key-env ENV_NAME, not raw key."
1558
1732
  return message
@@ -1665,20 +1839,14 @@ def sidebar_lines(context: McpContext) -> list[str]:
1665
1839
  "TASK",
1666
1840
  f" {trim(goal.current_task or '-', 24)}",
1667
1841
  "",
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",
1842
+ "COMMANDS",
1676
1843
  " / = command palette",
1677
- " /agents",
1678
- " /claude-install .",
1679
- " /models",
1680
- " /clear-models",
1681
- " /ask <prompt>",
1844
+ " /setup",
1845
+ " /status",
1846
+ " /goal",
1847
+ " /workflow",
1848
+ " /note <text>",
1849
+ " /clear",
1682
1850
  ]
1683
1851
 
1684
1852
 
@@ -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.34",
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.34"
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
+