agentram 0.1.32 → 0.1.37

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
@@ -439,6 +439,9 @@ def run_textual_tui(context: McpContext) -> bool:
439
439
  .setup-input { height: 3; }
440
440
  #chat { height: 1fr; border: solid #263241; padding: 1; overflow-y: scroll; }
441
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; }
442
445
  #input { dock: bottom; border: solid #3b82f6; }
443
446
  .muted { color: #8b9bb0; }
444
447
  """
@@ -496,6 +499,13 @@ def run_textual_tui(context: McpContext) -> bool:
496
499
  yield Button("Next", id="setup-next", variant="primary")
497
500
  yield Button("Skip", id="setup-skip")
498
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")
499
509
  yield Static(id="palette")
500
510
  yield Input(placeholder="Prompt or /command. Mouse wheel scroll in chat.", id="input")
501
511
  yield Footer()
@@ -661,7 +671,13 @@ def run_textual_tui(context: McpContext) -> bool:
661
671
  self.last_ctrl_c_at = now
662
672
 
663
673
  def on_button_pressed(self, event: Button.Pressed) -> None:
664
- if event.button.id == "setup-next":
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")
680
+ elif event.button.id == "setup-next":
665
681
  self.action_next_setup()
666
682
  elif event.button.id == "setup-prev":
667
683
  self.action_prev_setup()
@@ -670,6 +686,36 @@ def run_textual_tui(context: McpContext) -> bool:
670
686
  self.hide_setup()
671
687
  self.query_one("#input", Input).focus()
672
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
+
673
719
  def hide_palette(self) -> None:
674
720
  try:
675
721
  palette = self.query_one("#palette", Static)
@@ -705,10 +751,11 @@ def run_textual_tui(context: McpContext) -> bool:
705
751
  def on_input_changed(self, event: Input.Changed) -> None:
706
752
  if event.input.id != "input":
707
753
  return
708
- value = event.value.strip()
709
- if value.startswith("/"):
754
+ value = event.value
755
+ if value.startswith("/") and " " not in value:
710
756
  self.show_palette(value)
711
- else:
757
+ elif self.suggestions:
758
+ self.suggestions = []
712
759
  self.hide_palette()
713
760
 
714
761
  def action_accept_suggestion(self) -> None:
@@ -743,12 +790,14 @@ def run_textual_tui(context: McpContext) -> bool:
743
790
  if decision == "yes_once":
744
791
  approved = self.pending_approval_prompt
745
792
  self.pending_approval_prompt = None
793
+ self.hide_approval_popup()
746
794
  self.chat.write("agentram > approved once")
747
795
  self.enqueue_prompt(approved)
748
796
  return
749
797
  if decision == "yes_project":
750
798
  approved = self.pending_approval_prompt
751
799
  self.pending_approval_prompt = None
800
+ self.hide_approval_popup()
752
801
  self.project_main_agent_approved = True
753
802
  self.chat.write("agentram > approved for this project")
754
803
  self.enqueue_prompt(approved)
@@ -756,8 +805,9 @@ def run_textual_tui(context: McpContext) -> bool:
756
805
  if decision == "no":
757
806
  self.chat.write("agentram > denied; prompt dropped")
758
807
  self.pending_approval_prompt = None
808
+ self.hide_approval_popup()
759
809
  return
760
- self.chat.write("agentram > approval required: type yes, yes do this project, or no")
810
+ self.chat.write("agentram > approval required: click Yes / Yes, do this project / No")
761
811
  return
762
812
  if text and (not self.prompt_history or self.prompt_history[-1] != text):
763
813
  self.prompt_history.append(text)
@@ -769,12 +819,7 @@ def run_textual_tui(context: McpContext) -> bool:
769
819
  return
770
820
  if requires_main_agent_approval(self.context, text) and not self.project_main_agent_approved:
771
821
  self.pending_approval_prompt = text
772
- self.chat.write(
773
- "agentram > main agent wants terminal/file access. Allow?\n"
774
- " yes = allow once\n"
775
- " yes do this project = allow for this project\n"
776
- " no = deny"
777
- )
822
+ self.show_approval_popup(text)
778
823
  return
779
824
  self.enqueue_prompt(text)
780
825
 
@@ -1278,19 +1323,14 @@ def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
1278
1323
  plan = runtime_execution_plan(context, supervisor, prompt, memory_context, route)
1279
1324
  outputs["supervisor"] = plan
1280
1325
  outputs["small_after_plan"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
1281
- if main_agent:
1282
- try:
1283
- outputs["main"] = call_agent_endpoint(main_agent, build_main_agent_prompt(prompt, plan, memory_context))
1284
- except Exception as error: # noqa: BLE001 - agent boundary
1285
- outputs["main"] = f"error: {model_error_hint(str(error))}"
1286
- else:
1287
- outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
1288
- outputs["small"] = small_agent_observe(context, small_agent, prompt, "main_result", outputs.get("main", ""), plan, memory_context)
1326
+ loop_result = run_main_agent_loop(context, main_agent, small_agent, prompt, plan, memory_context)
1327
+ outputs.update(loop_result["outputs"])
1289
1328
  sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1290
1329
  persist_workflow_memory(context, prompt, plan, outputs)
1291
1330
  return "\n".join(
1292
1331
  [
1293
- f"Workflow: route={intent} confidence={float(route.get('confidence', 0.0)):.2f} -> main -> small memory note -> merge",
1332
+ f"Workflow: route={intent} confidence={float(route.get('confidence', 0.0)):.2f} -> main loop -> small memory note -> merge",
1333
+ f"Loop: steps={loop_result['steps']} status={loop_result['status']} retries={loop_result['retries']}",
1294
1334
  "Supervisor plan:",
1295
1335
  plan,
1296
1336
  "",
@@ -1305,6 +1345,104 @@ def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
1305
1345
  ]
1306
1346
  )
1307
1347
 
1348
+ def main_loop_retry_limit() -> int:
1349
+ return max(0, min(5, int(os.getenv("AGENTRAM_MAIN_MAX_RETRIES", "1"))))
1350
+
1351
+ def run_main_agent_loop(
1352
+ context: McpContext,
1353
+ main_agent: AgentModelEndpoint | None,
1354
+ small_agent: AgentModelEndpoint | None,
1355
+ prompt: str,
1356
+ plan: str,
1357
+ memory_context: str,
1358
+ ) -> dict[str, object]:
1359
+ outputs: dict[str, str] = {}
1360
+ if not main_agent:
1361
+ outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
1362
+ outputs["small"] = small_agent_observe(context, small_agent, prompt, "main_result", outputs["main"], plan, memory_context)
1363
+ return {"outputs": outputs, "steps": 0, "status": "no_main", "retries": 0}
1364
+
1365
+ max_retries = main_loop_retry_limit()
1366
+ retries = 0
1367
+ status = "running"
1368
+ step = 0
1369
+ previous_main_outputs: list[str] = []
1370
+ small_notes: list[str] = []
1371
+
1372
+ while True:
1373
+ step += 1
1374
+ step_prompt = build_main_loop_prompt(prompt, plan, memory_context, previous_main_outputs, small_notes, step)
1375
+ try:
1376
+ main_output = call_agent_endpoint(main_agent, step_prompt)
1377
+ except Exception as error: # noqa: BLE001 - agent boundary
1378
+ main_output = f"error: {model_error_hint(str(error))}"
1379
+ retries += 1
1380
+ previous_main_outputs.append(f"Step {step}: {main_output}")
1381
+ small_note = small_agent_observe(context, small_agent, prompt, f"main_error_step_{step}", main_output, plan, memory_context)
1382
+ small_notes.append(small_note)
1383
+ outputs[f"main_step_{step}"] = main_output
1384
+ outputs[f"small_step_{step}"] = small_note
1385
+ if retries > max_retries:
1386
+ status = "error_retry_limit"
1387
+ break
1388
+ continue
1389
+
1390
+ previous_main_outputs.append(f"Step {step}: {main_output}")
1391
+ small_note = small_agent_observe(context, small_agent, prompt, f"main_result_step_{step}", main_output, plan, memory_context)
1392
+ small_notes.append(small_note)
1393
+ outputs[f"main_step_{step}"] = main_output
1394
+ outputs[f"small_step_{step}"] = small_note
1395
+ decision = small_loop_decision(main_output, small_note)
1396
+ status = decision
1397
+ if decision in {"done", "blocker", "error"}:
1398
+ break
1399
+
1400
+ outputs["main"] = "\n\n".join(previous_main_outputs).strip() or "-"
1401
+ outputs["small"] = "\n\n".join(small_notes).strip() or "No small agent bound."
1402
+ return {"outputs": outputs, "steps": len(previous_main_outputs), "status": status, "retries": retries}
1403
+
1404
+ def build_main_loop_prompt(
1405
+ prompt: str,
1406
+ plan: str,
1407
+ memory_context: str,
1408
+ previous_main_outputs: list[str],
1409
+ small_notes: list[str],
1410
+ step: 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}.",
1416
+ "No hard step limit. Continue until user goal is complete or blocked.",
1417
+ "Do not stop early if unfinished work remains. Do not invent completion.",
1418
+ "If complete, include DONE. If blocked, include BLOCKER and reason.",
1419
+ "User request:",
1420
+ prompt,
1421
+ "Supervisor / execution plan:",
1422
+ plan,
1423
+ "Recent main/small feedback:",
1424
+ history,
1425
+ "AgentRAM context:",
1426
+ memory_context,
1427
+ ]
1428
+ )
1429
+
1430
+ def small_loop_decision(main_output: str, small_note: str) -> str:
1431
+ combined = f"{main_output}\n{small_note}".lower()
1432
+ if "error:" in main_output.lower():
1433
+ return "error"
1434
+ done_markers = ["loop_decision: done", "status: done", "done", "hoàn thành", "hoan thanh", "complete", "completed"]
1435
+ 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"]
1436
+ continue_markers = ["loop_decision: continue", "status: continue", "continue", "tiếp tục", "tiep tuc", "next step"]
1437
+ if any(marker in combined for marker in blocker_markers):
1438
+ return "blocker"
1439
+ if any(marker in combined for marker in continue_markers):
1440
+ return "continue"
1441
+ if any(marker in combined for marker in done_markers):
1442
+ return "done"
1443
+ return "done"
1444
+
1445
+
1308
1446
  def classify_agent_runtime_route(context: McpContext, prompt: str, main_agent: AgentModelEndpoint | None = None) -> dict[str, object]:
1309
1447
  if main_agent:
1310
1448
  try:
@@ -1458,7 +1596,7 @@ def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
1458
1596
  return "\n".join(["You are AgentRAM main coding agent.", "Nếu user prompt bằng tiếng Việt, trả lời bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "Follow supervisor plan. Focus on planning, orchestration, coding, refactor, bug fix, and final result.", "Do not invent repo facts. Mention files to inspect/change.", "User request:", prompt, "Supervisor plan:", plan, "AgentRAM context:", memory_context])
1459
1597
 
1460
1598
  def build_small_agent_prompt(prompt: str, plan: str, memory_context: str, event_content: str = "", event_type: str = "main_result") -> str:
1461
- return "\n".join(["You are AgentRAM small memory agent.", "Nếu user prompt bằng tiếng Việt, ghi note RAM bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "You participate in every AgentRAM event directly: user request, supervisor plan, main result, compact/merge.", "Do not edit code. Observe event content, then note decisions, risks, files, next tasks, and coding facts for RAM.", "You are responsible for setting the visible RAM goal/task from the latest event.", "Return concise memory notes only. Không dùng tiếng Anh nếu user đang dùng tiếng Việt.", "Bắt buộc gồm 2 dòng đầu theo format: current_goal: ... và current_task: ...", "User request:", prompt, "Supervisor plan:", plan, "Observed event type:", event_type, "Observed event content:", event_content or "-", "AgentRAM context:", memory_context])
1599
+ 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])
1462
1600
 
1463
1601
  def bind_router_text(context: McpContext, options: dict[str, object]) -> str:
1464
1602
  api_key_env = str(options.get("api_key_env", ""))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentram",
3
- "version": "0.1.32",
3
+ "version": "0.1.37",
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",
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.32"
7
+ version = "0.1.37"
8
8
  description = "Async, model-agnostic RAM layer for agentic coding tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"