agentram 0.1.21 → 0.1.30

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/agentram/cli.py CHANGED
@@ -1,4 +1,4 @@
1
- from __future__ import annotations
1
+ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import asyncio
@@ -101,6 +101,135 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
101
101
  ]
102
102
 
103
103
 
104
+
105
+ CLAUDE_COMPAT_COMMANDS: list[tuple[str, str]] = [
106
+ ("/add-dir <path>", "Claude: add a working directory for file access"),
107
+ ("/advisor [model|off]", "Claude: enable or disable advisor model"),
108
+ ("/autofix-pr [prompt]", "Claude: cloud session fixes PR CI/review comments"),
109
+ ("/background [prompt]", "Claude: detach session as background agent"),
110
+ ("/bg [prompt]", "Claude alias: /background"),
111
+ ("/batch <instruction>", "Claude workflow: parallel codebase change"),
112
+ ("/branch [name]", "Claude: fork conversation branch"),
113
+ ("/btw <question>", "Claude: side question without history"),
114
+ ("/cd <path>", "Claude: move session to another directory"),
115
+ ("/chrome", "Claude: configure Claude in Chrome"),
116
+ ("/claude-api [migrate|managed-agents-onboard]", "Claude skill: API reference and migration"),
117
+ ("/reset [name]", "Claude alias: /clear"),
118
+ ("/new [name]", "Claude alias: /clear"),
119
+ ("/code-review [level] [--fix] [--comment] [target]", "Claude skill: review current diff"),
120
+ ("/color [color|default]", "Claude: prompt bar color"),
121
+ ("/compact [instructions]", "Claude: summarize context to free space"),
122
+ ("/config [key=value ...]", "Claude: settings interface"),
123
+ ("/settings [key=value ...]", "Claude alias: /config"),
124
+ ("/context [all]", "Claude: visualize context usage"),
125
+ ("/copy [N]", "Claude: copy assistant response"),
126
+ ("/cost", "Claude alias: /usage"),
127
+ ("/dataviz [request]", "Claude skill: chart/dashboard design guidance"),
128
+ ("/debug [description]", "Claude skill: enable and inspect debug logs"),
129
+ ("/deep-research <question>", "Claude workflow: web research report"),
130
+ ("/design-login", "Claude: authorize design-system access"),
131
+ ("/design-sync [hint]", "Claude skill: sync React design system"),
132
+ ("/desktop", "Claude: continue in desktop app"),
133
+ ("/app", "Claude alias: /desktop"),
134
+ ("/diff", "Claude: interactive diff viewer"),
135
+ ("/doctor", "Claude: diagnose install/runtime"),
136
+ ("/effort [level|auto]", "Claude: set reasoning effort"),
137
+ ("/export [filename]", "Claude: export conversation"),
138
+ ("/fast [on|off]", "Claude: toggle fast mode"),
139
+ ("/feedback [report]", "Claude: send feedback"),
140
+ ("/bug [report]", "Claude alias: /feedback"),
141
+ ("/share [report]", "Claude alias: /feedback"),
142
+ ("/fewer-permission-prompts", "Claude skill: reduce permission prompts"),
143
+ ("/focus", "Claude: toggle focus view"),
144
+ ("/fork <directive>", "Claude: forked background subagent"),
145
+ ("/heapdump", "Claude: write heap snapshot"),
146
+ ("/hooks", "Claude: view hook config"),
147
+ ("/ide", "Claude: manage IDE integration"),
148
+ ("/insights", "Claude: analyze Claude Code sessions"),
149
+ ("/install-github-app", "Claude: install GitHub app"),
150
+ ("/install-slack-app", "Claude: install Slack app"),
151
+ ("/keybindings", "Claude: open keyboard shortcut file"),
152
+ ("/login", "Claude: sign in"),
153
+ ("/logout", "Claude: sign out"),
154
+ ("/loop [interval] [prompt]", "Claude skill: repeated prompt loop"),
155
+ ("/proactive [interval] [prompt]", "Claude alias: /loop"),
156
+ ("/mcp [reconnect|enable|disable]", "Claude: manage MCP servers"),
157
+ ("/memory", "Claude: edit CLAUDE.md memory"),
158
+ ("/mobile", "Claude: show mobile app QR"),
159
+ ("/ios", "Claude alias: /mobile"),
160
+ ("/android", "Claude alias: /mobile"),
161
+ ("/model [model]", "Claude: switch model"),
162
+ ("/passes", "Claude: share Claude Code pass"),
163
+ ("/permissions", "Claude: manage tool permissions"),
164
+ ("/allowed-tools", "Claude alias: /permissions"),
165
+ ("/plan [description]", "Claude: enter plan mode"),
166
+ ("/plugin [subcommand]", "Claude: manage plugins"),
167
+ ("/powerup", "Claude: feature lessons"),
168
+ ("/pr-comments [PR]", "Claude legacy: PR comments"),
169
+ ("/privacy-settings", "Claude: privacy settings"),
170
+ ("/radio", "Claude: Claude FM radio"),
171
+ ("/recap", "Claude: one-line session summary"),
172
+ ("/release-notes", "Claude: changelog picker"),
173
+ ("/reload-plugins [--force]", "Claude: reload plugins"),
174
+ ("/reload-skills", "Claude: reload skills and commands"),
175
+ ("/remote-control", "Claude: remote control from claude.ai"),
176
+ ("/rc", "Claude alias: /remote-control"),
177
+ ("/remote-env", "Claude: cloud agent environment"),
178
+ ("/rename [name]", "Claude: rename session"),
179
+ ("/resume [session]", "Claude: resume conversation"),
180
+ ("/continue [session]", "Claude alias: /resume"),
181
+ ("/review [PR]", "Claude: review GitHub PR"),
182
+ ("/rewind", "Claude: rewind conversation/code"),
183
+ ("/checkpoint", "Claude alias: /rewind"),
184
+ ("/undo", "Claude alias: /rewind"),
185
+ ("/run", "Claude skill: run app and verify change"),
186
+ ("/run-skill-generator", "Claude skill: generate run/verify skill"),
187
+ ("/sandbox", "Claude: toggle sandbox mode"),
188
+ ("/schedule [description]", "Claude: cloud routine"),
189
+ ("/routines [description]", "Claude alias: /schedule"),
190
+ ("/scroll-speed", "Claude: adjust mouse wheel speed"),
191
+ ("/security-review", "Claude: security review current diff"),
192
+ ("/setup-bedrock", "Claude: Bedrock setup wizard"),
193
+ ("/setup-vertex", "Claude: Vertex setup wizard"),
194
+ ("/simplify [target]", "Claude skill: cleanup review and fixes"),
195
+ ("/skills", "Claude: list skills"),
196
+ ("/stats", "Claude alias: /usage"),
197
+ ("/statusline", "Claude: configure status line"),
198
+ ("/stickers", "Claude: order stickers"),
199
+ ("/stop", "Claude: stop background session"),
200
+ ("/tasks", "Claude: view background work"),
201
+ ("/bashes", "Claude alias: /tasks"),
202
+ ("/team-onboarding", "Claude: generate team onboarding"),
203
+ ("/teleport", "Claude: pull web session into terminal"),
204
+ ("/tp", "Claude alias: /teleport"),
205
+ ("/terminal-setup", "Claude: terminal keybindings setup"),
206
+ ("/theme", "Claude: change color theme"),
207
+ ("/tui [default|fullscreen]", "Claude: switch renderer"),
208
+ ("/ultraplan <prompt>", "Claude: draft cloud plan"),
209
+ ("/ultrareview [PR]", "Claude: deep cloud review"),
210
+ ("/upgrade", "Claude: open upgrade page"),
211
+ ("/usage", "Claude: show usage and cost"),
212
+ ("/usage-credits", "Claude: configure usage credits"),
213
+ ("/verify", "Claude skill: verify app behavior"),
214
+ ("/vim", "Claude legacy: use /config for editor mode"),
215
+ ("/voice [hold|tap|off]", "Claude: voice dictation"),
216
+ ("/web-setup", "Claude: connect GitHub for web"),
217
+ ("/workflows", "Claude: workflow progress view"),
218
+ ]
219
+
220
+ CLAUDE_COMPAT_NAMES = {command.split()[0] for command, _ in CLAUDE_COMPAT_COMMANDS}
221
+
222
+ def slash_entries() -> list[tuple[str, str]]:
223
+ seen: set[str] = set()
224
+ entries: list[tuple[str, str]] = []
225
+ for command, description in [*SLASH_COMMANDS, *CLAUDE_COMPAT_COMMANDS]:
226
+ name = command.split()[0]
227
+ if name in seen:
228
+ continue
229
+ seen.add(name)
230
+ entries.append((command, description))
231
+ return entries
232
+
104
233
  SLASH_HELP = """Slash commands:
105
234
  /help Show commands
106
235
  /init Create .agentram files
@@ -346,13 +475,17 @@ def run_textual_tui(context: McpContext) -> bool:
346
475
  def __init__(self, mcp_context: McpContext) -> None:
347
476
  super().__init__()
348
477
  self.context = mcp_context
349
- self.messages: list[str] = []
350
- self.busy = False
351
- self.suggestions: list[str] = []
478
+ self.messages: list[str] = []
479
+ self.busy = False
480
+ self.prompt_queue: list[str] = []
481
+ self.processing_task: asyncio.Task[None] | None = None
482
+ self.suggestions: list[str] = []
352
483
  self.setup_done = False
353
484
  self.prompt_history: list[str] = []
354
485
  self.history_index: int | None = None
355
486
  self.last_ctrl_c_at = 0.0
487
+ self.setup_slots = ("supervisor", "main", "small")
488
+ self.setup_index = 0
356
489
 
357
490
  def compose(self) -> ComposeResult:
358
491
  yield Header(show_clock=True)
@@ -360,18 +493,20 @@ def run_textual_tui(context: McpContext) -> bool:
360
493
  yield Static(id="sidebar")
361
494
  with Vertical(id="main"):
362
495
  with Vertical(id="setup"):
363
- yield Static("Model setup - configure each agent separately", id="setup-title")
496
+ yield Static("Model setup - 1/3 Supervisor", id="setup-title")
364
497
  for slot, label, default_model in [
365
- ("supervisor", "Supervisor", ""),
366
- ("main", "Main coding", ""),
367
- ("small", "Small memory", ""),
498
+ ("supervisor", "1. Supervisor", ""),
499
+ ("main", "2. Main coding", ""),
500
+ ("small", "3. Small memory", ""),
368
501
  ]:
369
- with Vertical(classes="agent-setup"):
502
+ with Vertical(id=f"setup-{slot}-panel", classes="agent-setup"):
370
503
  yield Static(f"{label} agent")
371
504
  yield Select(
372
505
  [
373
506
  ("OpenAI Compatible", "openai-compatible"),
374
507
  ("Anthropic Compatible", "anthropic"),
508
+ ("Claude Code CLI", "claude-code"),
509
+ ("Codex CLI", "codex-cli"),
375
510
  ("Custom", "custom"),
376
511
  ],
377
512
  value="openai-compatible",
@@ -380,14 +515,16 @@ def run_textual_tui(context: McpContext) -> bool:
380
515
  )
381
516
  yield Input(placeholder="Custom provider if preset=Custom", id=f"setup-{slot}-custom-provider", classes="setup-input")
382
517
  yield Input(value=default_model, placeholder="Model name", id=f"setup-{slot}-model", classes="setup-input")
383
- yield Input(placeholder="Base URL, e.g. http://localhost:20128/v1", id=f"setup-{slot}-base-url", classes="setup-input")
518
+ yield Input(placeholder="Base URL or CLI command, e.g. http://localhost:20128/v1 / claude / codex exec", id=f"setup-{slot}-base-url", classes="setup-input")
384
519
  yield Input(value="OPENAI_API_KEY", placeholder="API key or env name", id=f"setup-{slot}-api-key-env", classes="setup-input")
385
520
  with Horizontal(id="setup-actions"):
386
- yield Button("Save setup", id="setup-save", variant="primary")
521
+ yield Button("Back", id="setup-prev")
522
+ yield Button("Next", id="setup-next", variant="primary")
523
+ yield Button("Save setup", id="setup-save", variant="success")
387
524
  yield Button("Skip", id="setup-skip")
388
525
  yield RichLog(id="chat", wrap=True, highlight=True, markup=False, auto_scroll=True)
389
526
  yield Static(id="palette")
390
- yield Input(placeholder="Prompt ho?c /command. Mouse wheel scroll trong khung chat.", id="input")
527
+ yield Input(placeholder="Prompt or /command. Mouse wheel scroll in chat.", id="input")
391
528
  yield Footer()
392
529
 
393
530
  def on_mount(self) -> None:
@@ -395,10 +532,11 @@ def run_textual_tui(context: McpContext) -> bool:
395
532
  self.chat.write("system > AgentRAM Textual TUI ready")
396
533
  self.chat.write(f"system > RAM root: {self.context.ram_root}")
397
534
  self.prefill_setup_from_profile()
535
+ self.refresh_setup_step()
398
536
  self.refresh_sidebar()
399
537
  self.set_interval(1.0, self.refresh_sidebar)
400
538
  try:
401
- self.query_one("#setup-supervisor-model", Input).focus()
539
+ self.focus_current_setup_model()
402
540
  except Exception: # noqa: BLE001 - fallback if setup is hidden/unmounted
403
541
  self.query_one("#input", Input).focus()
404
542
 
@@ -418,7 +556,7 @@ def run_textual_tui(context: McpContext) -> bool:
418
556
  if not endpoint:
419
557
  continue
420
558
  provider = endpoint.provider
421
- preset = provider if provider in {"openai-compatible", "anthropic"} else "custom"
559
+ preset = provider if provider in {"openai-compatible", "anthropic", "claude-code", "codex-cli"} else "custom"
422
560
  self.query_one(f"#setup-{slot}-provider", Select).value = preset
423
561
  self.query_one(f"#setup-{slot}-custom-provider", Input).value = "" if preset != "custom" else provider
424
562
  self.query_one(f"#setup-{slot}-model", Input).value = endpoint.model
@@ -426,6 +564,37 @@ def run_textual_tui(context: McpContext) -> bool:
426
564
  self.query_one(f"#setup-{slot}-api-key-env", Input).value = endpoint.api_key_env
427
565
  self.chat.write("system > Existing workflow models loaded. Save to update, or Skip to keep.")
428
566
 
567
+ def current_setup_slot(self) -> str:
568
+ return self.setup_slots[self.setup_index]
569
+
570
+ def current_setup_label(self) -> str:
571
+ labels = {"supervisor": "Supervisor", "main": "Main coding", "small": "Small memory"}
572
+ return labels[self.current_setup_slot()]
573
+
574
+ def refresh_setup_step(self) -> None:
575
+ total = len(self.setup_slots)
576
+ slot = self.current_setup_slot()
577
+ self.query_one("#setup-title", Static).update(f"Model setup - {self.setup_index + 1}/{total} {self.current_setup_label()}")
578
+ for item in self.setup_slots:
579
+ self.query_one(f"#setup-{item}-panel", Vertical).styles.display = "block" if item == slot else "none"
580
+
581
+ def focus_current_setup_model(self) -> None:
582
+ self.query_one(f"#setup-{self.current_setup_slot()}-model", Input).focus()
583
+
584
+ def action_next_setup(self) -> None:
585
+ if self.setup_index < len(self.setup_slots) - 1:
586
+ self.setup_index += 1
587
+ self.refresh_setup_step()
588
+ self.focus_current_setup_model()
589
+ else:
590
+ self.action_save_setup()
591
+
592
+ def action_prev_setup(self) -> None:
593
+ if self.setup_index > 0:
594
+ self.setup_index -= 1
595
+ self.refresh_setup_step()
596
+ self.focus_current_setup_model()
597
+
429
598
  def setup_value(self, widget_id: str) -> str:
430
599
  return self.query_one(f"#{widget_id}", Input).value.strip()
431
600
 
@@ -473,8 +642,10 @@ def run_textual_tui(context: McpContext) -> bool:
473
642
 
474
643
  def show_setup(self) -> None:
475
644
  self.setup_done = False
645
+ self.setup_index = 0
476
646
  self.query_one("#setup", Vertical).styles.display = "block"
477
- self.query_one("#setup-supervisor-model", Input).focus()
647
+ self.refresh_setup_step()
648
+ self.focus_current_setup_model()
478
649
 
479
650
  def action_history_prev(self) -> None:
480
651
  input_widget = self.query_one("#input", Input)
@@ -515,6 +686,10 @@ def run_textual_tui(context: McpContext) -> bool:
515
686
  def on_button_pressed(self, event: Button.Pressed) -> None:
516
687
  if event.button.id == "setup-save":
517
688
  self.action_save_setup()
689
+ elif event.button.id == "setup-next":
690
+ self.action_next_setup()
691
+ elif event.button.id == "setup-prev":
692
+ self.action_prev_setup()
518
693
  elif event.button.id == "setup-skip":
519
694
  self.chat.write("system > Model setup skipped. Use /bind-agent later.")
520
695
  self.hide_setup()
@@ -535,7 +710,7 @@ def run_textual_tui(context: McpContext) -> bool:
535
710
  return
536
711
  query = value.strip().lower().lstrip("/")
537
712
  matches = []
538
- for command, description in SLASH_COMMANDS:
713
+ for command, description in slash_entries():
539
714
  if not command.startswith("/"):
540
715
  continue
541
716
  command_name = command.split()[0]
@@ -547,7 +722,7 @@ def run_textual_tui(context: McpContext) -> bool:
547
722
  if not matches:
548
723
  palette.update("No matching slash command")
549
724
  else:
550
- lines = ["Slash commands ? Tab completes first match"]
725
+ lines = ["Slash commands - Tab completes first match"]
551
726
  lines.extend(f" {command.ljust(18)} {description}" for command, description in matches[:8])
552
727
  palette.update("\n".join(lines))
553
728
  palette.styles.display = "block"
@@ -571,7 +746,7 @@ def run_textual_tui(context: McpContext) -> bool:
571
746
 
572
747
  async def on_input_submitted(self, event: Input.Submitted) -> None:
573
748
  if event.input.id and str(event.input.id).startswith("setup-"):
574
- self.action_save_setup()
749
+ self.action_next_setup()
575
750
  return
576
751
  text = event.value.strip()
577
752
  event.input.value = ""
@@ -596,23 +771,37 @@ def run_textual_tui(context: McpContext) -> bool:
596
771
  self.chat.clear()
597
772
  self.chat.write("system > Chat cleared.")
598
773
  return
599
- if self.busy:
600
- self.chat.write("agentram > busy, wait current task")
601
- return
602
- self.busy = True
603
- self.chat.write("agentram > thinking...")
604
- try:
605
- response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
606
- except Exception as error: # noqa: BLE001 - TUI boundary
607
- response = f"error: {error}"
608
- finally:
609
- self.busy = False
610
- if response == "__clear__":
611
- self.chat.clear()
612
- self.chat.write("system > Chat cleared.")
613
- elif response:
614
- self.chat.write("agentram > " + response)
615
- self.refresh_sidebar()
774
+ if self.busy:
775
+ self.prompt_queue.append(text)
776
+ self.chat.write(f"agentram > queued #{len(self.prompt_queue)}")
777
+ return
778
+ self.prompt_queue.append(text)
779
+ self.processing_task = asyncio.create_task(self.process_prompt_queue())
780
+
781
+ async def process_prompt_queue(self) -> None:
782
+ if self.busy:
783
+ return
784
+ self.busy = True
785
+ try:
786
+ while self.prompt_queue:
787
+ text = self.prompt_queue.pop(0)
788
+ pending = len(self.prompt_queue)
789
+ suffix = f" ({pending} queued)" if pending else ""
790
+ self.chat.write(f"agentram > thinking...{suffix}")
791
+ try:
792
+ response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
793
+ except Exception as error: # noqa: BLE001 - TUI boundary
794
+ response = f"error: {error}"
795
+ if response == "__clear__":
796
+ self.chat.clear()
797
+ self.chat.write("system > Chat cleared.")
798
+ elif response:
799
+ self.chat.write("agentram > " + response)
800
+ self.refresh_sidebar()
801
+ finally:
802
+ self.busy = False
803
+ self.processing_task = None
804
+ self.query_one("#input", Input).focus()
616
805
 
617
806
  AgentRAMTextualApp(context).run()
618
807
  return True
@@ -631,11 +820,11 @@ def run_with_activity(context: McpContext, messages: list[str], user_input: str)
631
820
 
632
821
 
633
822
  def slash_command_names() -> list[str]:
634
- return [command.split()[0] for command, _ in SLASH_COMMANDS if command.startswith("/")]
823
+ return [command.split()[0] for command, _ in slash_entries() if command.startswith("/")]
635
824
 
636
825
 
637
826
  def slash_command_meta() -> dict[str, str]:
638
- return {command.split()[0]: description for command, description in SLASH_COMMANDS if command.startswith("/")}
827
+ return {command.split()[0]: description for command, description in slash_entries() if command.startswith("/")}
639
828
 
640
829
 
641
830
  def create_prompt_session() -> Any | None:
@@ -655,7 +844,7 @@ def create_prompt_session() -> Any | None:
655
844
  completer=completer,
656
845
  complete_while_typing=True,
657
846
  reserve_space_for_menu=12,
658
- bottom_toolbar="Type / for slash commands · /agents for subagents · /ask to call bound models",
847
+ bottom_toolbar="Type / for slash commands | /agents for subagents | /ask to call bound models",
659
848
  )
660
849
 
661
850
 
@@ -755,9 +944,25 @@ def handle_slash_command(context: McpContext, text: str, messages: list[str] | N
755
944
  return note_text(context, raw_rest)
756
945
  if command == "/clear":
757
946
  return "__clear__"
947
+ if command in CLAUDE_COMPAT_NAMES:
948
+ return claude_compat_command_text(command, raw_rest)
758
949
  return f"Unknown slash command: {command}\n\n{SLASH_HELP}"
759
950
 
760
951
 
952
+ def claude_compat_command_text(command: str, arguments: str) -> str:
953
+ descriptions = slash_command_meta()
954
+ description = descriptions.get(command, "Claude Code command")
955
+ return "\n".join(
956
+ [
957
+ f"Claude-compatible command: {command}",
958
+ f"Purpose: {description}",
959
+ f"Args: {arguments or '-'}",
960
+ "Status: listed for Claude parity in AgentRAM TUI.",
961
+ "Native execution is not implemented here unless AgentRAM has its own handler for this command.",
962
+ "Run the same command inside Claude Code for Claude-native behavior.",
963
+ ]
964
+ )
965
+
761
966
  def init_text(context: McpContext) -> str:
762
967
  data = payload(call_tool(context, "agentram_init", {}))
763
968
  created = data.get("created", [])
@@ -852,7 +1057,7 @@ def agents_text() -> str:
852
1057
  def slash_palette_text(filter_text: str = "") -> str:
853
1058
  normalized = filter_text.strip().lower().lstrip("/")
854
1059
  rows = []
855
- for command, description in SLASH_COMMANDS:
1060
+ for command, description in slash_entries():
856
1061
  command_key = command.lower().lstrip("/")
857
1062
  if normalized and normalized not in command_key:
858
1063
  continue
@@ -993,45 +1198,192 @@ def workflow_text(context: McpContext) -> str:
993
1198
  lines.append(f"- {slot}: not bound")
994
1199
  return "\n".join(lines)
995
1200
 
996
- def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
997
- agents = context.profile_store.list_agents()
998
- supervisor = agents.get("supervisor")
999
- main_agent = agents.get("main")
1000
- small_agent = agents.get("small")
1001
- memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
1002
- plan = supervisor_plan(supervisor, prompt, memory_context)
1003
- outputs: dict[str, str] = {}
1004
- with ThreadPoolExecutor(max_workers=2) as executor:
1005
- futures = {}
1006
- if main_agent:
1007
- futures[executor.submit(call_agent_endpoint, main_agent, build_main_agent_prompt(prompt, plan, memory_context))] = "main"
1008
- if small_agent:
1009
- futures[executor.submit(call_agent_endpoint, small_agent, build_small_agent_prompt(prompt, plan, memory_context))] = "small"
1010
- for future, slot in list(futures.items()):
1011
- try:
1012
- outputs[slot] = future.result()
1013
- except Exception as error: # noqa: BLE001 - agent boundary
1014
- outputs[slot] = f"error: {model_error_hint(str(error))}"
1015
- if not main_agent:
1016
- outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
1017
- persist_workflow_memory(context, prompt, plan, outputs)
1018
- return "\n".join(
1019
- [
1020
- "Workflow: supervisor -> main + small -> merge",
1021
- "Supervisor plan:",
1022
- plan,
1023
- "",
1024
- "Main agent result:",
1025
- outputs.get("main", "-"),
1026
- "",
1027
- "Small memory agent notes:",
1028
- outputs.get("small", "No small agent bound."),
1029
- ]
1030
- )
1031
-
1032
- def persist_workflow_memory(context: McpContext, prompt: str, plan: str, outputs: dict[str, str]) -> None:
1033
- small_notes = outputs.get("small", "").strip()
1034
- main_result = outputs.get("main", "").strip()
1201
+ def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
1202
+ agents = context.profile_store.list_agents()
1203
+ supervisor = agents.get("supervisor")
1204
+ main_agent = agents.get("main")
1205
+ small_agent = agents.get("small")
1206
+ memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
1207
+ route = classify_agent_runtime_route(context, prompt, main_agent)
1208
+ intent = str(route.get("intent", "chat"))
1209
+ outputs: dict[str, str] = {}
1210
+
1211
+ if intent == "planning":
1212
+ plan = supervisor_plan(supervisor, prompt, memory_context)
1213
+ outputs["supervisor"] = plan
1214
+ outputs["small"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
1215
+ sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1216
+ persist_workflow_memory(context, prompt, plan, outputs)
1217
+ return "\n".join(
1218
+ [
1219
+ f"Workflow: route=planning confidence={float(route.get('confidence', 0.0)):.2f} -> supervisor -> small memory note -> merge",
1220
+ "Supervisor plan:",
1221
+ plan,
1222
+ "",
1223
+ "Small memory agent notes:",
1224
+ outputs.get("small", "No small agent bound."),
1225
+ ]
1226
+ )
1227
+
1228
+ if intent == "memory":
1229
+ plan = "Memory-only event. Small agent writes RAM note directly."
1230
+ outputs["small"] = small_agent_observe(context, small_agent, prompt, "memory_event", prompt, plan, memory_context)
1231
+ sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1232
+ persist_workflow_memory(context, prompt, plan, outputs)
1233
+ return "\n".join(
1234
+ [
1235
+ f"Workflow: route=memory confidence={float(route.get('confidence', 0.0)):.2f} -> small memory note -> merge",
1236
+ "Small memory agent notes:",
1237
+ outputs.get("small", "No small agent bound."),
1238
+ ]
1239
+ )
1240
+
1241
+ plan = runtime_execution_plan(context, supervisor, prompt, memory_context, route)
1242
+ outputs["supervisor"] = plan
1243
+ outputs["small_after_plan"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
1244
+ if main_agent:
1245
+ try:
1246
+ outputs["main"] = call_agent_endpoint(main_agent, build_main_agent_prompt(prompt, plan, memory_context))
1247
+ except Exception as error: # noqa: BLE001 - agent boundary
1248
+ outputs["main"] = f"error: {model_error_hint(str(error))}"
1249
+ else:
1250
+ outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
1251
+ outputs["small"] = small_agent_observe(context, small_agent, prompt, "main_result", outputs.get("main", ""), plan, memory_context)
1252
+ sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1253
+ persist_workflow_memory(context, prompt, plan, outputs)
1254
+ return "\n".join(
1255
+ [
1256
+ f"Workflow: route={intent} confidence={float(route.get('confidence', 0.0)):.2f} -> main -> small memory note -> merge",
1257
+ "Supervisor plan:",
1258
+ plan,
1259
+ "",
1260
+ "Main agent result:",
1261
+ outputs.get("main", "-"),
1262
+ "",
1263
+ "Small memory agent after plan:",
1264
+ outputs.get("small_after_plan", "No small agent bound."),
1265
+ "",
1266
+ "Small memory agent notes:",
1267
+ outputs.get("small", "No small agent bound."),
1268
+ ]
1269
+ )
1270
+
1271
+ def classify_agent_runtime_route(context: McpContext, prompt: str, main_agent: AgentModelEndpoint | None = None) -> dict[str, object]:
1272
+ if main_agent:
1273
+ try:
1274
+ response = call_agent_endpoint(main_agent, build_agent_runtime_router_prompt(prompt))
1275
+ data = json.loads(extract_json_object(response))
1276
+ intent = str(data.get("intent", "")).strip().lower()
1277
+ if intent in {"planning", "execution", "memory", "chat"}:
1278
+ return {
1279
+ "intent": intent,
1280
+ "confidence": float(data.get("confidence", 0.75)),
1281
+ "reason": str(data.get("reason", "main-agent router")),
1282
+ "source": "main-agent",
1283
+ }
1284
+ except Exception as error: # noqa: BLE001 - router boundary
1285
+ fallback = heuristic_agent_runtime_route(prompt)
1286
+ fallback["source"] = "heuristic"
1287
+ fallback["router_error"] = str(error)
1288
+ return fallback
1289
+
1290
+ return heuristic_agent_runtime_route(prompt)
1291
+
1292
+ def heuristic_agent_runtime_route(prompt: str) -> dict[str, object]:
1293
+ lower = prompt.lower()
1294
+ execute_words = [
1295
+ "bắt đầu làm", "bat dau lam", "làm đi", "lam di", "tiến hành", "tien hanh", "thực hiện", "thuc hien",
1296
+ "chốt kế hoạch", "chot ke hoach", "bắt tay", "start", "execute", "implement", "do it", "go ahead",
1297
+ ]
1298
+ plan_words = ["lên kế hoạch", "len ke hoach", "kế hoạch", "ke hoach", "plan", "roadmap", "milestone", "thiết kế", "thiet ke"]
1299
+ if any(word in lower for word in execute_words):
1300
+ return {"intent": "execution", "confidence": 0.9, "source": "heuristic"}
1301
+ if any(word in lower for word in plan_words):
1302
+ return {"intent": "planning", "confidence": 0.85, "source": "heuristic"}
1303
+ decision = heuristic_intent(prompt)
1304
+ intent = str(decision.get("intent", "chat"))
1305
+ if intent == "coding":
1306
+ decision["intent"] = "execution"
1307
+ decision["source"] = "heuristic"
1308
+ return decision
1309
+
1310
+ def build_agent_runtime_router_prompt(prompt: str) -> str:
1311
+ return "\n".join(
1312
+ [
1313
+ "You are AgentRAM orchestration router using the main agent model.",
1314
+ "Classify which agent should handle the user prompt. Do not edit files. Do not run tools. Return only JSON.",
1315
+ "Schema: {\"intent\":\"planning|execution|memory|chat\",\"confidence\":0.0,\"reason\":\"short\"}",
1316
+ "planning = user asks to plan, design, set roadmap, discuss goal, no execution yet.",
1317
+ "execution = user approves plan, says start/do it, or asks coding/refactor/fix/test/file work.",
1318
+ "memory = user asks to remember/save/note a decision/fact.",
1319
+ "chat = normal conversation.",
1320
+ "Nếu user prompt bằng tiếng Việt, reason dùng tiếng Việt.",
1321
+ "User prompt:",
1322
+ prompt,
1323
+ ]
1324
+ )
1325
+
1326
+ def runtime_execution_plan(context: McpContext, supervisor: AgentModelEndpoint | None, prompt: str, memory_context: str, route: dict[str, object]) -> str:
1327
+ state = context.goal_store.load().context_block()
1328
+ if state:
1329
+ return "\n".join(["Use current accepted Goal Stack as execution plan.", state])
1330
+ if str(route.get("intent")) == "execution" and any(word in prompt.lower() for word in ["bắt đầu", "bat dau", "làm đi", "lam di", "execute", "do it"]):
1331
+ return "User approved previous plan. Use AgentRAM memory context and current prompt to execute next concrete task."
1332
+ return supervisor_plan(supervisor, prompt, memory_context)
1333
+
1334
+ def small_agent_observe(context: McpContext, small_agent: AgentModelEndpoint | None, prompt: str, event_type: str, event_content: str, plan: str, memory_context: str) -> str:
1335
+ if not small_agent:
1336
+ return "No small agent bound."
1337
+ try:
1338
+ return call_agent_endpoint(
1339
+ small_agent,
1340
+ build_small_agent_prompt(prompt, plan, memory_context, event_content, event_type=event_type),
1341
+ )
1342
+ except Exception as error: # noqa: BLE001 - agent boundary
1343
+ return f"error: {model_error_hint(str(error))}"
1344
+
1345
+ def sync_goal_state_from_small_agent(context: McpContext, prompt: str, plan: str, small_notes: str) -> None:
1346
+ if not small_notes.strip() or small_notes.strip().startswith("error:"):
1347
+ return
1348
+ state = context.goal_store.load()
1349
+ goal = extract_labeled_value(small_notes, ("current_goal", "goal", "mục tiêu", "muc tieu")) or first_nonempty_line(plan)
1350
+ task = extract_labeled_value(small_notes, ("current_task", "task", "nhiệm vụ", "nhiem vu")) or first_nonempty_line(small_notes) or prompt
1351
+ patch: dict[str, object] = {
1352
+ "current_task": trim(task.strip(), 180),
1353
+ "goal_history": [trim(goal.strip() or prompt.strip(), 180)],
1354
+ }
1355
+ if not state.mission:
1356
+ patch["mission"] = "Xây dựng và vận hành AgentRAM cho coding agents."
1357
+ if not state.project_goal:
1358
+ patch["project_goal"] = "AgentRAM workflow: supervisor lập kế hoạch, main coding agent thực thi, small memory agent ghi RAM."
1359
+ if goal:
1360
+ patch["current_goal"] = trim(goal.strip(), 180)
1361
+ context.goal_store.update(patch)
1362
+
1363
+ def extract_labeled_value(text: str, labels: tuple[str, ...]) -> str:
1364
+ for line in text.splitlines():
1365
+ cleaned = line.strip().lstrip("-*").strip()
1366
+ for label in labels:
1367
+ pattern = rf"^{re.escape(label)}\s*[:=]\s*(.+)$"
1368
+ match = re.match(pattern, cleaned, flags=re.IGNORECASE)
1369
+ if match:
1370
+ return match.group(1).strip().strip("`*_-")
1371
+ return ""
1372
+
1373
+ def first_nonempty_line(value: str) -> str:
1374
+ for line in value.splitlines():
1375
+ cleaned = line.strip().lstrip("-0123456789. )")
1376
+ if cleaned and not cleaned.lower().startswith(("supervisor error", "fallback plan")):
1377
+ return cleaned
1378
+ return ""
1379
+
1380
+ def persist_workflow_memory(context: McpContext, prompt: str, plan: str, outputs: dict[str, str]) -> None:
1381
+ small_notes = "\n".join(
1382
+ value.strip()
1383
+ for key, value in outputs.items()
1384
+ if key.startswith("small") and value.strip() and not value.strip().startswith("error:")
1385
+ )
1386
+ main_result = outputs.get("main", "").strip()
1035
1387
  note_parts = [
1036
1388
  f"User request: {prompt}",
1037
1389
  f"Supervisor plan: {trim(plan.strip(), 1200)}" if plan.strip() else "Supervisor plan: -",
@@ -1062,14 +1414,14 @@ def supervisor_plan(supervisor: AgentModelEndpoint | None, prompt: str, memory_c
1062
1414
  def call_agent_endpoint(endpoint: AgentModelEndpoint, prompt: str) -> str:
1063
1415
  return MultiModelCodingOrchestrator([endpoint])._call_endpoint(endpoint, prompt)
1064
1416
 
1065
- def build_supervisor_prompt(prompt: str, memory_context: str) -> str:
1066
- return "\n".join(["You are AgentRAM supervisor. Act like a senior coding-agent boss.", "Create concise plan for main coding agent and small memory agent.", "Return only actionable plan. No markdown table.", "User request:", prompt, "AgentRAM context:", memory_context])
1067
-
1068
- def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
1069
- return "\n".join(["You are AgentRAM main coding agent.", "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])
1070
-
1071
- def build_small_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
1072
- return "\n".join(["You are AgentRAM small memory agent.", "Do not edit code. Note plans, decisions, risks, files, next tasks, and coding facts for RAM.", "Return concise memory notes only.", "User request:", prompt, "Supervisor plan:", plan, "AgentRAM context:", memory_context])
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])
1073
1425
 
1074
1426
  def bind_router_text(context: McpContext, options: dict[str, object]) -> str:
1075
1427
  api_key_env = str(options.get("api_key_env", ""))
@@ -1154,9 +1506,9 @@ def extract_json_object(text: str) -> str:
1154
1506
 
1155
1507
  def heuristic_intent(prompt: str) -> dict[str, object]:
1156
1508
  lower = prompt.lower()
1157
- memory_words = ["ghi nhớ", "remember", "note", "decision", "lưu lại", "save this"]
1158
- planning_words = ["goal", "kế hoạch", "ke hoach", "milestone", "mission", "drift", "next task", "roadmap", "mục tiêu", "muc tieu"]
1159
- coding_words = ["sửa", "sua", "fix", "bug", "code", "test", "refactor", "review", "file", ".py", ".js", "endpoint", "class", "function"]
1509
+ memory_words = ["ghi nhớ", "remember", "note", "decision", "lưu lại", "save this"]
1510
+ planning_words = ["goal", "kế hoạch", "ke hoach", "milestone", "mission", "drift", "next task", "roadmap", "mục tiêu", "muc tieu"]
1511
+ coding_words = ["sá»­a", "sua", "fix", "bug", "code", "test", "refactor", "review", "file", ".py", ".js", "endpoint", "class", "function"]
1160
1512
  if any(word in lower for word in memory_words):
1161
1513
  return {"intent": "memory", "confidence": 0.75}
1162
1514
  if any(word in lower for word in planning_words):
@@ -1191,12 +1543,16 @@ def model_error_hint(message: str) -> str:
1191
1543
  return message + " | Fix: /remove-model <id> or re-bind with --base-url http://localhost:8000/v1"
1192
1544
  if "actively refused" in lower or "connection refused" in lower or "winerror 10061" in lower:
1193
1545
  return message + " | Fix: start your OpenAI-compatible server at base_url, or change --base-url."
1546
+ if "http 403" in lower or "403" in lower and "groq" in lower:
1547
+ return message + " | Fix: Groq rejected auth/model. Rotate/check API key, ensure provider=openai-compatible, base_url=https://api.groq.com/openai/v1, and use a Groq-supported model name."
1194
1548
  if "http 404" in lower or "404: not found" in lower:
1195
1549
  return message + " | Fix: verify base_url includes /v1, provider supports /chat/completions, and model name exists."
1196
1550
  if "http 400" in lower and "not supported" in lower:
1197
1551
  return message + " | Fix: this account/provider rejects that model. Bind a supported model name from your local endpoint."
1198
1552
  if "returned non-json" in lower or "expecting value" in lower:
1199
1553
  return message + " | Fix: endpoint returned empty/HTML/non-JSON. Check base_url, auth key, proxy, and server logs."
1554
+ 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."
1200
1556
  if "missing api key env" in lower:
1201
1557
  return message + " | Fix: set environment variable, then bind using --api-key-env ENV_NAME, not raw key."
1202
1558
  return message
@@ -1273,7 +1629,7 @@ def draw_screen(context: McpContext, messages: list[str], activity: str | None =
1273
1629
  left_width = 30
1274
1630
  right_width = width - left_width - 3
1275
1631
  print("+" + "-" * (width - 2) + "+")
1276
- print(box_text("AgentRAM Code | RAM + Claude subagents + slash commands", width))
1632
+ print(box_text("AgentRAM Code | RAM + Claude subagents + slash commands", width))
1277
1633
  print("+" + "-" * (width - 2) + "+")
1278
1634
 
1279
1635
  sidebar = sidebar_lines(context)
@@ -1328,7 +1684,7 @@ def sidebar_lines(context: McpContext) -> list[str]:
1328
1684
 
1329
1685
 
1330
1686
  def trim(text: str, limit: int) -> str:
1331
- return text if len(text) <= limit else text[: limit - 1] + ""
1687
+ return text if len(text) <= limit else text[: limit - 3] + "..."
1332
1688
 
1333
1689
 
1334
1690
  def menu_lines() -> list[str]: