agentram 0.1.32 → 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/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)
@@ -743,12 +789,14 @@ def run_textual_tui(context: McpContext) -> bool:
743
789
  if decision == "yes_once":
744
790
  approved = self.pending_approval_prompt
745
791
  self.pending_approval_prompt = None
792
+ self.hide_approval_popup()
746
793
  self.chat.write("agentram > approved once")
747
794
  self.enqueue_prompt(approved)
748
795
  return
749
796
  if decision == "yes_project":
750
797
  approved = self.pending_approval_prompt
751
798
  self.pending_approval_prompt = None
799
+ self.hide_approval_popup()
752
800
  self.project_main_agent_approved = True
753
801
  self.chat.write("agentram > approved for this project")
754
802
  self.enqueue_prompt(approved)
@@ -756,8 +804,9 @@ def run_textual_tui(context: McpContext) -> bool:
756
804
  if decision == "no":
757
805
  self.chat.write("agentram > denied; prompt dropped")
758
806
  self.pending_approval_prompt = None
807
+ self.hide_approval_popup()
759
808
  return
760
- self.chat.write("agentram > approval required: type yes, yes do this project, or no")
809
+ self.chat.write("agentram > approval required: click Yes / Yes, do this project / No")
761
810
  return
762
811
  if text and (not self.prompt_history or self.prompt_history[-1] != text):
763
812
  self.prompt_history.append(text)
@@ -769,12 +818,7 @@ def run_textual_tui(context: McpContext) -> bool:
769
818
  return
770
819
  if requires_main_agent_approval(self.context, text) and not self.project_main_agent_approved:
771
820
  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
- )
821
+ self.show_approval_popup(text)
778
822
  return
779
823
  self.enqueue_prompt(text)
780
824
 
@@ -1278,19 +1322,14 @@ def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
1278
1322
  plan = runtime_execution_plan(context, supervisor, prompt, memory_context, route)
1279
1323
  outputs["supervisor"] = plan
1280
1324
  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)
1325
+ loop_result = run_main_agent_loop(context, main_agent, small_agent, prompt, plan, memory_context)
1326
+ outputs.update(loop_result["outputs"])
1289
1327
  sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
1290
1328
  persist_workflow_memory(context, prompt, plan, outputs)
1291
1329
  return "\n".join(
1292
1330
  [
1293
- f"Workflow: route={intent} confidence={float(route.get('confidence', 0.0)):.2f} -> main -> small memory note -> merge",
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']}",
1294
1333
  "Supervisor plan:",
1295
1334
  plan,
1296
1335
  "",
@@ -1305,6 +1344,104 @@ def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
1305
1344
  ]
1306
1345
  )
1307
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
+
1308
1445
  def classify_agent_runtime_route(context: McpContext, prompt: str, main_agent: AgentModelEndpoint | None = None) -> dict[str, object]:
1309
1446
  if main_agent:
1310
1447
  try:
@@ -1458,7 +1595,7 @@ def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
1458
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])
1459
1596
 
1460
1597
  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])
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])
1462
1599
 
1463
1600
  def bind_router_text(context: McpContext, options: dict[str, object]) -> str:
1464
1601
  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.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",
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.34"
8
8
  description = "Async, model-agnostic RAM layer for agentic coding tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"