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 +3 -2
- package/agentram/cli.py +488 -320
- 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
|
|
|
@@ -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"), ("
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
596
|
+
def save_setup_slot(self, slot: str) -> str:
|
|
608
597
|
role_by_slot = {"supervisor": "supervisor", "main": "coder", "small": "memory"}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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 == "
|
|
688
|
-
self.
|
|
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.
|
|
775
|
-
self.
|
|
776
|
-
self.
|
|
777
|
-
return
|
|
778
|
-
self.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
|
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
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
"
|
|
1258
|
-
plan,
|
|
1259
|
-
"",
|
|
1260
|
-
"
|
|
1261
|
-
|
|
1262
|
-
"",
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
def
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
-
return
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
if
|
|
1356
|
-
|
|
1357
|
-
if
|
|
1358
|
-
|
|
1359
|
-
if
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
-
"
|
|
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
|
-
" /
|
|
1678
|
-
" /
|
|
1679
|
-
" /
|
|
1680
|
-
" /
|
|
1681
|
-
" /
|
|
1844
|
+
" /setup",
|
|
1845
|
+
" /status",
|
|
1846
|
+
" /goal",
|
|
1847
|
+
" /workflow",
|
|
1848
|
+
" /note <text>",
|
|
1849
|
+
" /clear",
|
|
1682
1850
|
]
|
|
1683
1851
|
|
|
1684
1852
|
|
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.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.
|
|
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
|
+
|