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 +3 -2
- package/agentram/cli.py +343 -258
- package/agentram/mcp_server.py +1 -1
- package/agentram/orchestration.py +8 -6
- package/agentram/storage.py +100 -79
- package/package.json +2 -1
- package/pyproject.toml +2 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
("/
|
|
74
|
+
("/setup", "Open model setup"),
|
|
75
75
|
("/status", "Show RAM status"),
|
|
76
|
-
("/
|
|
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
|
|
79
|
-
("/
|
|
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", "
|
|
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
|
|
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
|
-
/
|
|
221
|
+
/setup Open model setup
|
|
236
222
|
/status Show RAM status
|
|
237
|
-
/
|
|
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
|
|
240
|
-
/
|
|
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
|
|
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"), ("
|
|
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.
|
|
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.
|
|
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
|
|
586
|
+
def save_setup_slot(self, slot: str) -> str:
|
|
606
587
|
role_by_slot = {"supervisor": "supervisor", "main": "coder", "small": "memory"}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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-
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
784
|
-
self.
|
|
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
|
|
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
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
return
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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
|
-
"
|
|
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
|
-
" /
|
|
1624
|
-
" /
|
|
1625
|
-
" /
|
|
1626
|
-
" /
|
|
1627
|
-
" /
|
|
1707
|
+
" /setup",
|
|
1708
|
+
" /status",
|
|
1709
|
+
" /goal",
|
|
1710
|
+
" /workflow",
|
|
1711
|
+
" /note <text>",
|
|
1712
|
+
" /clear",
|
|
1628
1713
|
]
|
|
1629
1714
|
|
|
1630
1715
|
|
package/agentram/mcp_server.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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")),
|
package/agentram/storage.py
CHANGED
|
@@ -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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
data.setdefault("
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
self.
|
|
103
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
|