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 +155 -18
- package/package.json +1 -1
- package/pyproject.toml +1 -1
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 == "
|
|
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:
|
|
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.
|
|
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
|
-
|
|
1282
|
-
|
|
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