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 +159 -21
- 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)
|
|
@@ -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
|
|
709
|
-
if value.startswith("/"):
|
|
754
|
+
value = event.value
|
|
755
|
+
if value.startswith("/") and " " not in value:
|
|
710
756
|
self.show_palette(value)
|
|
711
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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)
|
|
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