agentram 0.1.8 → 0.1.10
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/README.md +11 -0
- package/agentram/cli.py +115 -26
- package/agentram/orchestration.py +8 -1
- package/package.json +1 -1
- package/pyproject.toml +2 -2
package/README.md
CHANGED
|
@@ -998,3 +998,14 @@ Supervisor creates plan -> Main agent executes -> Small agent records RAM notes
|
|
|
998
998
|
```
|
|
999
999
|
|
|
1000
1000
|
Small agent should not own big direction. It records plans, decisions, files, risks, next tasks, and coding facts into RAM.
|
|
1001
|
+
|
|
1002
|
+
## Textual TUI
|
|
1003
|
+
|
|
1004
|
+
Install rich terminal UI with native scrollable chat pane:
|
|
1005
|
+
|
|
1006
|
+
```bash
|
|
1007
|
+
pip install agentram[tui]
|
|
1008
|
+
agentram tui
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
When `textual` is installed, AgentRAM uses a real full-screen TUI with mouse wheel scrolling inside the chat frame. If `textual` is missing, it falls back to the basic print-based terminal UI.
|
package/agentram/cli.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import asyncio
|
|
4
5
|
from concurrent.futures import ThreadPoolExecutor
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
@@ -44,10 +45,10 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
|
|
|
44
45
|
("/workflow", "Show supervisor workflow agents"),
|
|
45
46
|
("/route on|off|status", "Toggle auto prompt routing"),
|
|
46
47
|
("/orchestrate <task>", "Build multi-model agent plan"),
|
|
47
|
-
("/ask <prompt>", "Execute bound models and return outputs"),
|
|
48
|
-
("/claude-install [path]", "Install .claude commands and agents"),
|
|
49
|
-
("/note <text>", "Save decision note"),
|
|
50
|
-
("/clear", "Clear chat"),
|
|
48
|
+
("/ask <prompt>", "Execute bound models and return outputs"),
|
|
49
|
+
("/claude-install [path]", "Install .claude commands and agents"),
|
|
50
|
+
("/note <text>", "Save decision note"),
|
|
51
|
+
("/clear", "Clear chat"),
|
|
51
52
|
("/exit", "Quit"),
|
|
52
53
|
("Plain text", "Capture coding chat message and retrieve context"),
|
|
53
54
|
]
|
|
@@ -76,9 +77,9 @@ SLASH_HELP = """Slash commands:
|
|
|
76
77
|
/route on|off|status Toggle auto prompt routing
|
|
77
78
|
/orchestrate <task> Build multi-model agent plan
|
|
78
79
|
/ask <prompt> Execute bound models and return outputs
|
|
79
|
-
/claude-install [path] Install .claude commands and agents
|
|
80
|
-
/note <text> Save decision note
|
|
81
|
-
/clear Clear chat
|
|
80
|
+
/claude-install [path] Install .claude commands and agents
|
|
81
|
+
/note <text> Save decision note
|
|
82
|
+
/clear Clear chat
|
|
82
83
|
/exit Quit
|
|
83
84
|
Plain text Capture coding chat message and retrieve context
|
|
84
85
|
""".strip()
|
|
@@ -237,34 +238,121 @@ def run_command(args: argparse.Namespace) -> int:
|
|
|
237
238
|
raise SystemExit(f"unknown command: {command}")
|
|
238
239
|
|
|
239
240
|
|
|
240
|
-
def run_tui(context: McpContext) -> None:
|
|
241
|
-
context
|
|
241
|
+
def run_tui(context: McpContext) -> None:
|
|
242
|
+
if run_textual_tui(context):
|
|
243
|
+
return
|
|
244
|
+
context.init_storage()
|
|
242
245
|
prompt_session = create_prompt_session()
|
|
243
246
|
input_hint = "live slash autocomplete enabled" if prompt_session else "basic input fallback; install prompt_toolkit for live slash dropdown"
|
|
244
|
-
messages = [
|
|
245
|
-
"system > AgentRAM coding-agent CLI ready",
|
|
246
|
-
f"system > RAM root: {context.ram_root}",
|
|
247
|
-
f"system > {input_hint}",
|
|
248
|
-
]
|
|
249
|
-
while True:
|
|
250
|
-
draw_screen(context, messages)
|
|
247
|
+
messages = [
|
|
248
|
+
"system > AgentRAM coding-agent CLI ready",
|
|
249
|
+
f"system > RAM root: {context.ram_root}",
|
|
250
|
+
f"system > {input_hint}",
|
|
251
|
+
]
|
|
252
|
+
while True:
|
|
253
|
+
draw_screen(context, messages)
|
|
251
254
|
try:
|
|
252
255
|
user_input = read_tui_input(prompt_session).strip()
|
|
253
256
|
except (EOFError, KeyboardInterrupt):
|
|
254
257
|
return
|
|
255
|
-
if user_input in {"0", "q", "quit", "exit", "/exit", "/quit"}:
|
|
256
|
-
return
|
|
257
|
-
if user_input in BUTTONS:
|
|
258
|
-
user_input = BUTTONS[user_input]
|
|
259
|
-
if user_input:
|
|
260
|
-
messages.append(f"user > {user_input}")
|
|
258
|
+
if user_input in {"0", "q", "quit", "exit", "/exit", "/quit"}:
|
|
259
|
+
return
|
|
260
|
+
if user_input in BUTTONS:
|
|
261
|
+
user_input = BUTTONS[user_input]
|
|
262
|
+
if user_input:
|
|
263
|
+
messages.append(f"user > {user_input}")
|
|
261
264
|
response = run_with_activity(context, messages, user_input)
|
|
262
265
|
if response == "__clear__":
|
|
263
266
|
messages.clear()
|
|
264
267
|
messages.append("system > Chat cleared.")
|
|
265
268
|
elif response:
|
|
266
269
|
messages.append("agentram > " + response)
|
|
267
|
-
del messages[:-
|
|
270
|
+
del messages[:-200]
|
|
271
|
+
|
|
272
|
+
def run_textual_tui(context: McpContext) -> bool:
|
|
273
|
+
try:
|
|
274
|
+
from textual.app import App, ComposeResult
|
|
275
|
+
from textual.containers import Horizontal, Vertical
|
|
276
|
+
from textual.widgets import Footer, Header, Input, RichLog, Static
|
|
277
|
+
except ImportError:
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
class AgentRAMTextualApp(App[None]):
|
|
281
|
+
CSS = """
|
|
282
|
+
Screen { background: #0b0f14; color: #d7dde8; }
|
|
283
|
+
#layout { height: 1fr; }
|
|
284
|
+
#sidebar { width: 32; border: solid #263241; padding: 1; }
|
|
285
|
+
#main { width: 1fr; }
|
|
286
|
+
#chat { height: 1fr; border: solid #263241; padding: 1; overflow-y: scroll; }
|
|
287
|
+
#input { dock: bottom; border: solid #3b82f6; }
|
|
288
|
+
.muted { color: #8b9bb0; }
|
|
289
|
+
"""
|
|
290
|
+
BINDINGS = [("ctrl+c", "quit", "Quit")]
|
|
291
|
+
|
|
292
|
+
def __init__(self, mcp_context: McpContext) -> None:
|
|
293
|
+
super().__init__()
|
|
294
|
+
self.context = mcp_context
|
|
295
|
+
self.messages: list[str] = []
|
|
296
|
+
self.busy = False
|
|
297
|
+
|
|
298
|
+
def compose(self) -> ComposeResult:
|
|
299
|
+
yield Header(show_clock=True)
|
|
300
|
+
with Horizontal(id="layout"):
|
|
301
|
+
yield Static(id="sidebar")
|
|
302
|
+
with Vertical(id="main"):
|
|
303
|
+
yield RichLog(id="chat", wrap=True, highlight=True, markup=False, auto_scroll=True)
|
|
304
|
+
yield Input(placeholder="Prompt hoặc /command. Mouse wheel scroll trong khung chat.", id="input")
|
|
305
|
+
yield Footer()
|
|
306
|
+
|
|
307
|
+
def on_mount(self) -> None:
|
|
308
|
+
self.context.init_storage()
|
|
309
|
+
self.chat.write("system > AgentRAM Textual TUI ready")
|
|
310
|
+
self.chat.write(f"system > RAM root: {self.context.ram_root}")
|
|
311
|
+
self.refresh_sidebar()
|
|
312
|
+
self.set_interval(1.0, self.refresh_sidebar)
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def chat(self) -> RichLog:
|
|
316
|
+
return self.query_one("#chat", RichLog)
|
|
317
|
+
|
|
318
|
+
def refresh_sidebar(self) -> None:
|
|
319
|
+
self.query_one("#sidebar", Static).update("\n".join(sidebar_lines(self.context)))
|
|
320
|
+
|
|
321
|
+
async def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
322
|
+
text = event.value.strip()
|
|
323
|
+
event.input.value = ""
|
|
324
|
+
if not text:
|
|
325
|
+
return
|
|
326
|
+
if text in {"0", "q", "quit", "exit", "/exit", "/quit"}:
|
|
327
|
+
self.exit()
|
|
328
|
+
return
|
|
329
|
+
if text in BUTTONS:
|
|
330
|
+
text = BUTTONS[text]
|
|
331
|
+
self.chat.write(f"user > {text}")
|
|
332
|
+
if text == "/clear":
|
|
333
|
+
self.chat.clear()
|
|
334
|
+
self.chat.write("system > Chat cleared.")
|
|
335
|
+
return
|
|
336
|
+
if self.busy:
|
|
337
|
+
self.chat.write("agentram > busy, wait current task")
|
|
338
|
+
return
|
|
339
|
+
self.busy = True
|
|
340
|
+
self.chat.write("agentram > thinking...")
|
|
341
|
+
try:
|
|
342
|
+
response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
|
|
343
|
+
except Exception as error: # noqa: BLE001 - TUI boundary
|
|
344
|
+
response = f"error: {error}"
|
|
345
|
+
finally:
|
|
346
|
+
self.busy = False
|
|
347
|
+
if response == "__clear__":
|
|
348
|
+
self.chat.clear()
|
|
349
|
+
self.chat.write("system > Chat cleared.")
|
|
350
|
+
elif response:
|
|
351
|
+
self.chat.write("agentram > " + response)
|
|
352
|
+
self.refresh_sidebar()
|
|
353
|
+
|
|
354
|
+
AgentRAMTextualApp(context).run()
|
|
355
|
+
return True
|
|
268
356
|
|
|
269
357
|
def run_with_activity(context: McpContext, messages: list[str], user_input: str) -> str:
|
|
270
358
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
@@ -908,7 +996,7 @@ def draw_screen(context: McpContext, messages: list[str], activity: str | None =
|
|
|
908
996
|
print(box_text("◆ AgentRAM Code | RAM + Claude subagents + slash commands", width))
|
|
909
997
|
print("+" + "-" * (width - 2) + "+")
|
|
910
998
|
|
|
911
|
-
sidebar = sidebar_lines(context)
|
|
999
|
+
sidebar = sidebar_lines(context)
|
|
912
1000
|
display_messages = [*messages, activity] if activity else messages
|
|
913
1001
|
chat = wrap_messages(display_messages, right_width - 4)[-22:]
|
|
914
1002
|
rows = max(len(sidebar), len(chat), 22)
|
|
@@ -917,7 +1005,7 @@ def draw_screen(context: McpContext, messages: list[str], activity: str | None =
|
|
|
917
1005
|
right = chat[index] if index < len(chat) else ""
|
|
918
1006
|
print("| " + left[: left_width - 2].ljust(left_width - 2) + "| " + right[: right_width - 2].ljust(right_width - 2) + "|")
|
|
919
1007
|
print("+" + "-" * (left_width - 1) + "+" + "-" * (right_width) + "+")
|
|
920
|
-
footer = "Thinking/acting..." if activity else "Input: type / for
|
|
1008
|
+
footer = "Thinking/acting..." if activity else "Input: type / for commands. Use mouse wheel or terminal scrollbar for previous output."
|
|
921
1009
|
print(box_text(footer, width))
|
|
922
1010
|
print("+" + "-" * (width - 2) + "+")
|
|
923
1011
|
|
|
@@ -957,7 +1045,8 @@ def sidebar_lines(context: McpContext) -> list[str]:
|
|
|
957
1045
|
" /ask <prompt>",
|
|
958
1046
|
]
|
|
959
1047
|
|
|
960
|
-
|
|
1048
|
+
|
|
1049
|
+
|
|
961
1050
|
def trim(text: str, limit: int) -> str:
|
|
962
1051
|
return text if len(text) <= limit else text[: limit - 1] + "…"
|
|
963
1052
|
|
|
@@ -4,6 +4,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import re
|
|
7
8
|
import shlex
|
|
8
9
|
import subprocess
|
|
9
10
|
import urllib.error
|
|
@@ -219,4 +220,10 @@ def parse_openai_sse(raw: str) -> str:
|
|
|
219
220
|
message = choice.get("message", {})
|
|
220
221
|
if isinstance(message, dict) and message.get("content"):
|
|
221
222
|
parts.append(str(message.get("content")))
|
|
222
|
-
return "".join(parts).strip()
|
|
223
|
+
return strip_thinking_blocks("".join(parts)).strip()
|
|
224
|
+
|
|
225
|
+
def strip_thinking_blocks(text: str) -> str:
|
|
226
|
+
text = re.sub(r"<thinking>.*?</thinking>", "", text, flags=re.DOTALL | re.IGNORECASE)
|
|
227
|
+
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL | re.IGNORECASE)
|
|
228
|
+
text = re.sub(r"^\s*</?(thinking|think)>\s*", "", text, flags=re.IGNORECASE)
|
|
229
|
+
return text.strip()
|
package/package.json
CHANGED
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.
|
|
7
|
+
version = "0.1.10"
|
|
8
8
|
description = "Async, model-agnostic RAM layer for agentic coding tools."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -22,7 +22,7 @@ requires-python = ">=3.10"
|
|
|
22
22
|
dependencies = []
|
|
23
23
|
|
|
24
24
|
[project.optional-dependencies]
|
|
25
|
-
tui = ["prompt_toolkit>=3.0"]
|
|
25
|
+
tui = ["prompt_toolkit>=3.0", "textual>=0.80"]
|
|
26
26
|
|
|
27
27
|
[project.urls]
|
|
28
28
|
Homepage = "https://github.com/your-org/agentram"
|