agentram 0.1.13 → 0.1.15
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 +555 -441
- package/package.json +52 -52
- package/pyproject.toml +1 -1
package/agentram/cli.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import argparse
|
|
4
|
-
import asyncio
|
|
5
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
|
-
import shlex
|
|
9
|
-
import shutil
|
|
10
|
-
import textwrap
|
|
11
|
-
import time
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shlex
|
|
9
|
+
import shutil
|
|
10
|
+
import textwrap
|
|
11
|
+
import time
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
-
from .config import default_ram_root
|
|
16
|
-
from .mcp_server import McpContext, call_tool
|
|
17
|
-
from .orchestration import AgentModelEndpoint, MultiModelCodingOrchestrator
|
|
15
|
+
from .config import default_ram_root
|
|
16
|
+
from .mcp_server import McpContext, call_tool
|
|
17
|
+
from .orchestration import AgentModelEndpoint, MultiModelCodingOrchestrator
|
|
18
18
|
|
|
19
19
|
SUBAGENTS: dict[str, str] = {
|
|
20
20
|
"memory": "Summarize important facts, decisions, files, risks, and next tasks into AgentRAM events. Do not edit code.",
|
|
@@ -38,17 +38,17 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
|
|
|
38
38
|
("/models", "List bound coding models"),
|
|
39
39
|
("/remove-model <id>", "Remove bound coding model"),
|
|
40
40
|
("/disable-model <id>", "Disable bound coding model"),
|
|
41
|
-
("/clear-models", "Remove all bound coding models"),
|
|
42
|
-
("/bind-model provider model", "Bind model endpoint"),
|
|
43
|
-
("/bind-router provider model", "Bind intent router model"),
|
|
44
|
-
("/bind-agent slot provider model", "Bind supervisor/main/small agent"),
|
|
45
|
-
("/workflow", "Show supervisor workflow agents"),
|
|
46
|
-
("/route on|off|status", "Toggle auto prompt routing"),
|
|
47
|
-
("/orchestrate <task>", "Build multi-model agent plan"),
|
|
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"),
|
|
41
|
+
("/clear-models", "Remove all bound coding models"),
|
|
42
|
+
("/bind-model provider model", "Bind model endpoint"),
|
|
43
|
+
("/bind-router provider model", "Bind intent router model"),
|
|
44
|
+
("/bind-agent slot provider model", "Bind supervisor/main/small agent"),
|
|
45
|
+
("/workflow", "Show supervisor workflow agents"),
|
|
46
|
+
("/route on|off|status", "Toggle auto prompt routing"),
|
|
47
|
+
("/orchestrate <task>", "Build multi-model agent plan"),
|
|
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"),
|
|
52
52
|
("/exit", "Quit"),
|
|
53
53
|
("Plain text", "Capture coding chat message and retrieve context"),
|
|
54
54
|
]
|
|
@@ -69,20 +69,20 @@ SLASH_HELP = """Slash commands:
|
|
|
69
69
|
/models List bound coding models
|
|
70
70
|
/remove-model <id> Remove bound coding model
|
|
71
71
|
/disable-model <id> Disable bound coding model
|
|
72
|
-
/clear-models Remove all bound coding models
|
|
73
|
-
/bind-model provider model Bind model endpoint
|
|
74
|
-
/bind-router provider model Bind intent router model
|
|
75
|
-
/bind-agent slot provider model Bind supervisor/main/small agent
|
|
76
|
-
/workflow Show supervisor workflow agents
|
|
77
|
-
/route on|off|status Toggle auto prompt routing
|
|
78
|
-
/orchestrate <task> Build multi-model agent plan
|
|
72
|
+
/clear-models Remove all bound coding models
|
|
73
|
+
/bind-model provider model Bind model endpoint
|
|
74
|
+
/bind-router provider model Bind intent router model
|
|
75
|
+
/bind-agent slot provider model Bind supervisor/main/small agent
|
|
76
|
+
/workflow Show supervisor workflow agents
|
|
77
|
+
/route on|off|status Toggle auto prompt routing
|
|
78
|
+
/orchestrate <task> Build multi-model agent plan
|
|
79
79
|
/ask <prompt> Execute bound models and return outputs
|
|
80
|
-
/claude-install [path] Install .claude commands and agents
|
|
81
|
-
/note <text> Save decision note
|
|
82
|
-
/clear Clear chat
|
|
80
|
+
/claude-install [path] Install .claude commands and agents
|
|
81
|
+
/note <text> Save decision note
|
|
82
|
+
/clear Clear chat
|
|
83
83
|
/exit Quit
|
|
84
|
-
Plain text Capture coding chat message and retrieve context
|
|
85
|
-
""".strip()
|
|
84
|
+
Plain text Capture coding chat message and retrieve context
|
|
85
|
+
""".strip()
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -105,30 +105,30 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
105
105
|
disable_model.add_argument("id", help="Endpoint id.")
|
|
106
106
|
|
|
107
107
|
bind_model = subparsers.add_parser("bind-model", help="Bind a coding model endpoint.")
|
|
108
|
-
bind_model.add_argument("provider", help="Provider name, e.g. claude, openai-compatible, groq, claude-subagent.")
|
|
109
|
-
bind_model.add_argument("model", help="Model name or Claude subagent name.")
|
|
108
|
+
bind_model.add_argument("provider", help="Provider name, e.g. claude, openai-compatible, groq, claude-subagent.")
|
|
109
|
+
bind_model.add_argument("model", help="Model name or Claude subagent name.")
|
|
110
110
|
bind_model.add_argument("--role", default="coder", help="Agent role.")
|
|
111
111
|
bind_model.add_argument("--base-url", default="", help="Provider base URL.")
|
|
112
|
-
bind_model.add_argument("--api-key-env", default="", help="Environment variable containing API key.")
|
|
113
|
-
bind_model.add_argument("--modalities", default="text", help="Comma-separated modalities.")
|
|
114
|
-
|
|
115
|
-
bind_router = subparsers.add_parser("bind-router", help="Bind a separate intent router model endpoint.")
|
|
116
|
-
bind_router.add_argument("provider", help="Provider name, e.g. openai-compatible, groq, claude-subagent.")
|
|
117
|
-
bind_router.add_argument("model", help="Router model name.")
|
|
118
|
-
bind_router.add_argument("--base-url", default="", help="Provider base URL or local command for claude-subagent.")
|
|
119
|
-
bind_router.add_argument("--api-key-env", default="", help="Environment variable containing API key.")
|
|
120
|
-
|
|
121
|
-
bind_agent = subparsers.add_parser("bind-agent", help="Bind supervisor/main/small workflow agent.")
|
|
122
|
-
bind_agent.add_argument("slot", choices=["supervisor", "main", "small"], help="Workflow slot.")
|
|
123
|
-
bind_agent.add_argument("provider", help="Provider name, e.g. claude-subagent, openai-compatible, groq.")
|
|
124
|
-
bind_agent.add_argument("model", help="Model or subagent name.")
|
|
125
|
-
bind_agent.add_argument("--base-url", default="", help="Provider base URL or local command for claude-subagent.")
|
|
126
|
-
bind_agent.add_argument("--api-key-env", default="", help="Environment variable containing API key.")
|
|
127
|
-
|
|
128
|
-
subparsers.add_parser("workflow", help="Show supervisor/main/small workflow agents.")
|
|
129
|
-
|
|
130
|
-
route = subparsers.add_parser("route", help="Toggle or show auto prompt routing.")
|
|
131
|
-
route.add_argument("mode", nargs="?", default="status", choices=["on", "off", "status"], help="Auto route mode.")
|
|
112
|
+
bind_model.add_argument("--api-key-env", default="", help="Environment variable containing API key.")
|
|
113
|
+
bind_model.add_argument("--modalities", default="text", help="Comma-separated modalities.")
|
|
114
|
+
|
|
115
|
+
bind_router = subparsers.add_parser("bind-router", help="Bind a separate intent router model endpoint.")
|
|
116
|
+
bind_router.add_argument("provider", help="Provider name, e.g. openai-compatible, groq, claude-subagent.")
|
|
117
|
+
bind_router.add_argument("model", help="Router model name.")
|
|
118
|
+
bind_router.add_argument("--base-url", default="", help="Provider base URL or local command for claude-subagent.")
|
|
119
|
+
bind_router.add_argument("--api-key-env", default="", help="Environment variable containing API key.")
|
|
120
|
+
|
|
121
|
+
bind_agent = subparsers.add_parser("bind-agent", help="Bind supervisor/main/small workflow agent.")
|
|
122
|
+
bind_agent.add_argument("slot", choices=["supervisor", "main", "small"], help="Workflow slot.")
|
|
123
|
+
bind_agent.add_argument("provider", help="Provider name, e.g. claude-subagent, openai-compatible, groq.")
|
|
124
|
+
bind_agent.add_argument("model", help="Model or subagent name.")
|
|
125
|
+
bind_agent.add_argument("--base-url", default="", help="Provider base URL or local command for claude-subagent.")
|
|
126
|
+
bind_agent.add_argument("--api-key-env", default="", help="Environment variable containing API key.")
|
|
127
|
+
|
|
128
|
+
subparsers.add_parser("workflow", help="Show supervisor/main/small workflow agents.")
|
|
129
|
+
|
|
130
|
+
route = subparsers.add_parser("route", help="Toggle or show auto prompt routing.")
|
|
131
|
+
route.add_argument("mode", nargs="?", default="status", choices=["on", "off", "status"], help="Auto route mode.")
|
|
132
132
|
|
|
133
133
|
orchestrate = subparsers.add_parser("orchestrate", help="Build or run multi-model coding orchestration.")
|
|
134
134
|
orchestrate.add_argument("task", nargs="?", default="", help="Coding task.")
|
|
@@ -209,21 +209,21 @@ def run_command(args: argparse.Namespace) -> int:
|
|
|
209
209
|
if command == "clear-models":
|
|
210
210
|
print(clear_models_text(context))
|
|
211
211
|
return 0
|
|
212
|
-
if command == "bind-model":
|
|
213
|
-
print(bind_model_text(context, vars(args)))
|
|
214
|
-
return 0
|
|
215
|
-
if command == "bind-router":
|
|
216
|
-
print(bind_router_text(context, vars(args)))
|
|
217
|
-
return 0
|
|
218
|
-
if command == "bind-agent":
|
|
219
|
-
print(bind_agent_text(context, vars(args)))
|
|
220
|
-
return 0
|
|
221
|
-
if command == "workflow":
|
|
222
|
-
print(workflow_text(context))
|
|
223
|
-
return 0
|
|
224
|
-
if command == "route":
|
|
225
|
-
print(route_text(context, args.mode))
|
|
226
|
-
return 0
|
|
212
|
+
if command == "bind-model":
|
|
213
|
+
print(bind_model_text(context, vars(args)))
|
|
214
|
+
return 0
|
|
215
|
+
if command == "bind-router":
|
|
216
|
+
print(bind_router_text(context, vars(args)))
|
|
217
|
+
return 0
|
|
218
|
+
if command == "bind-agent":
|
|
219
|
+
print(bind_agent_text(context, vars(args)))
|
|
220
|
+
return 0
|
|
221
|
+
if command == "workflow":
|
|
222
|
+
print(workflow_text(context))
|
|
223
|
+
return 0
|
|
224
|
+
if command == "route":
|
|
225
|
+
print(route_text(context, args.mode))
|
|
226
|
+
return 0
|
|
227
227
|
if command == "orchestrate":
|
|
228
228
|
task = args.task or input("Task > ").strip()
|
|
229
229
|
print(orchestrate_text(context, task, files=args.file, execute=args.execute))
|
|
@@ -238,132 +238,246 @@ def run_command(args: argparse.Namespace) -> int:
|
|
|
238
238
|
raise SystemExit(f"unknown command: {command}")
|
|
239
239
|
|
|
240
240
|
|
|
241
|
-
def run_tui(context: McpContext) -> None:
|
|
242
|
-
if run_textual_tui(context):
|
|
243
|
-
return
|
|
244
|
-
context.init_storage()
|
|
241
|
+
def run_tui(context: McpContext) -> None:
|
|
242
|
+
if run_textual_tui(context):
|
|
243
|
+
return
|
|
244
|
+
context.init_storage()
|
|
245
245
|
prompt_session = create_prompt_session()
|
|
246
246
|
input_hint = "live slash autocomplete enabled" if prompt_session else "basic input fallback; install prompt_toolkit for live slash dropdown"
|
|
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)
|
|
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)
|
|
254
254
|
try:
|
|
255
255
|
user_input = read_tui_input(prompt_session).strip()
|
|
256
256
|
except (EOFError, KeyboardInterrupt):
|
|
257
257
|
return
|
|
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}")
|
|
264
|
-
response = run_with_activity(context, messages, 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}")
|
|
264
|
+
response = run_with_activity(context, messages, user_input)
|
|
265
265
|
if response == "__clear__":
|
|
266
266
|
messages.clear()
|
|
267
267
|
messages.append("system > Chat cleared.")
|
|
268
268
|
elif response:
|
|
269
269
|
messages.append("agentram > " + response)
|
|
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
|
-
#
|
|
287
|
-
#
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
self.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
self.
|
|
332
|
-
if
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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 Button, 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
|
+
#setup { height: auto; border: solid #3b82f6; padding: 1; background: #101826; }
|
|
287
|
+
#setup-title { height: auto; color: #dbeafe; }
|
|
288
|
+
#setup-actions { height: auto; }
|
|
289
|
+
.setup-input { height: 3; }
|
|
290
|
+
#chat { height: 1fr; border: solid #263241; padding: 1; overflow-y: scroll; }
|
|
291
|
+
#palette { height: auto; max-height: 12; border: solid #3b82f6; padding: 1; background: #101826; display: none; }
|
|
292
|
+
#input { dock: bottom; border: solid #3b82f6; }
|
|
293
|
+
.muted { color: #8b9bb0; }
|
|
294
|
+
"""
|
|
295
|
+
BINDINGS = [("ctrl+c", "quit", "Quit"), ("tab", "accept_suggestion", "Complete slash"), ("ctrl+s", "save_setup", "Save setup")]
|
|
296
|
+
|
|
297
|
+
def __init__(self, mcp_context: McpContext) -> None:
|
|
298
|
+
super().__init__()
|
|
299
|
+
self.context = mcp_context
|
|
300
|
+
self.messages: list[str] = []
|
|
301
|
+
self.busy = False
|
|
302
|
+
self.suggestions: list[str] = []
|
|
303
|
+
self.setup_done = False
|
|
304
|
+
|
|
305
|
+
def compose(self) -> ComposeResult:
|
|
306
|
+
yield Header(show_clock=True)
|
|
307
|
+
with Horizontal(id="layout"):
|
|
308
|
+
yield Static(id="sidebar")
|
|
309
|
+
with Vertical(id="main"):
|
|
310
|
+
yield RichLog(id="chat", wrap=True, highlight=True, markup=False, auto_scroll=True)
|
|
311
|
+
yield Input(placeholder="Prompt hoặc /command. Mouse wheel scroll trong khung chat.", id="input")
|
|
312
|
+
yield Footer()
|
|
313
|
+
|
|
314
|
+
def on_mount(self) -> None:
|
|
315
|
+
self.context.init_storage()
|
|
316
|
+
self.chat.write("system > AgentRAM Textual TUI ready")
|
|
317
|
+
self.chat.write(f"system > RAM root: {self.context.ram_root}")
|
|
318
|
+
self.prefill_setup_from_profile()
|
|
319
|
+
self.refresh_sidebar()
|
|
320
|
+
self.set_interval(1.0, self.refresh_sidebar)
|
|
321
|
+
self.query_one("#setup-provider", Input).focus()
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def chat(self) -> RichLog:
|
|
325
|
+
return self.query_one("#chat", RichLog)
|
|
326
|
+
|
|
327
|
+
def refresh_sidebar(self) -> None:
|
|
328
|
+
self.query_one("#sidebar", Static).update("\n".join(sidebar_lines(self.context)))
|
|
329
|
+
|
|
330
|
+
def prefill_setup_from_profile(self) -> None:
|
|
331
|
+
agents = self.context.profile_store.list_agents()
|
|
332
|
+
if not agents:
|
|
333
|
+
return
|
|
334
|
+
first = next(iter(agents.values()))
|
|
335
|
+
self.query_one("#setup-provider", Input).value = first.provider
|
|
336
|
+
self.query_one("#setup-base-url", Input).value = first.base_url
|
|
337
|
+
self.query_one("#setup-api-key-env", Input).value = first.api_key_env
|
|
338
|
+
for slot in ("supervisor", "main", "small"):
|
|
339
|
+
endpoint = agents.get(slot)
|
|
340
|
+
if endpoint:
|
|
341
|
+
self.query_one(f"#setup-{slot}", Input).value = endpoint.model
|
|
342
|
+
self.chat.write("system > Existing workflow models loaded. Save to update, or Skip to keep.")
|
|
343
|
+
|
|
344
|
+
def setup_value(self, widget_id: str) -> str:
|
|
345
|
+
return self.query_one(f"#{widget_id}", Input).value.strip()
|
|
346
|
+
|
|
347
|
+
def action_save_setup(self) -> None:
|
|
348
|
+
provider = self.setup_value("setup-provider") or "openai-compatible"
|
|
349
|
+
base_url = self.setup_value("setup-base-url")
|
|
350
|
+
api_key_env = self.setup_value("setup-api-key-env")
|
|
351
|
+
if api_key_env.startswith(("sk-", "gsk_", "sk_")):
|
|
352
|
+
self.chat.write("agentram > setup error: api key env must be variable name, not raw key")
|
|
353
|
+
return
|
|
354
|
+
role_by_slot = {"supervisor": "supervisor", "main": "coder", "small": "memory"}
|
|
355
|
+
bound: list[str] = []
|
|
356
|
+
for slot, role in role_by_slot.items():
|
|
357
|
+
model = self.setup_value(f"setup-{slot}")
|
|
358
|
+
if not model:
|
|
359
|
+
continue
|
|
360
|
+
endpoint = AgentModelEndpoint(
|
|
361
|
+
provider=provider,
|
|
362
|
+
model=model,
|
|
363
|
+
role=role,
|
|
364
|
+
base_url=base_url,
|
|
365
|
+
api_key_env=api_key_env,
|
|
366
|
+
modalities=["text"],
|
|
367
|
+
)
|
|
368
|
+
self.context.profile_store.set_agent(slot, endpoint)
|
|
369
|
+
bound.append(f"{slot}={model}")
|
|
370
|
+
if not bound:
|
|
371
|
+
self.chat.write("agentram > setup skipped: no model entered")
|
|
372
|
+
else:
|
|
373
|
+
self.chat.write("agentram > setup saved: " + ", ".join(bound))
|
|
374
|
+
self.hide_setup()
|
|
375
|
+
self.refresh_sidebar()
|
|
376
|
+
self.query_one("#input", Input).focus()
|
|
377
|
+
|
|
378
|
+
def hide_setup(self) -> None:
|
|
379
|
+
self.setup_done = True
|
|
380
|
+
self.query_one("#setup", Vertical).styles.display = "none"
|
|
381
|
+
|
|
382
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
383
|
+
if event.button.id == "setup-save":
|
|
384
|
+
self.action_save_setup()
|
|
385
|
+
elif event.button.id == "setup-skip":
|
|
386
|
+
self.chat.write("system > Model setup skipped. Use /bind-agent later.")
|
|
387
|
+
self.hide_setup()
|
|
388
|
+
self.query_one("#input", Input).focus()
|
|
389
|
+
|
|
390
|
+
def hide_palette(self) -> None:
|
|
391
|
+
palette = self.query_one("#palette", Static)
|
|
392
|
+
palette.update("")
|
|
393
|
+
palette.styles.display = "none"
|
|
394
|
+
|
|
395
|
+
def show_palette(self, value: str) -> None:
|
|
396
|
+
palette = self.query_one("#palette", Static)
|
|
397
|
+
query = value.strip().lower().lstrip("/")
|
|
398
|
+
matches = []
|
|
399
|
+
for command, description in SLASH_COMMANDS:
|
|
400
|
+
if not command.startswith("/"):
|
|
401
|
+
continue
|
|
402
|
+
command_name = command.split()[0]
|
|
403
|
+
haystack = command_name.lower().lstrip("/")
|
|
404
|
+
if query and query not in haystack:
|
|
405
|
+
continue
|
|
406
|
+
matches.append((command_name, description))
|
|
407
|
+
self.suggestions = [command for command, _ in matches[:8]]
|
|
408
|
+
if not matches:
|
|
409
|
+
palette.update("No matching slash command")
|
|
410
|
+
else:
|
|
411
|
+
lines = ["Slash commands ? Tab completes first match"]
|
|
412
|
+
lines.extend(f" {command.ljust(18)} {description}" for command, description in matches[:8])
|
|
413
|
+
palette.update("\n".join(lines))
|
|
414
|
+
palette.styles.display = "block"
|
|
415
|
+
|
|
416
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
417
|
+
value = event.value.strip()
|
|
418
|
+
if value.startswith("/"):
|
|
419
|
+
self.show_palette(value)
|
|
420
|
+
else:
|
|
421
|
+
self.hide_palette()
|
|
422
|
+
|
|
423
|
+
def action_accept_suggestion(self) -> None:
|
|
424
|
+
input_widget = self.query_one("#input", Input)
|
|
425
|
+
if not input_widget.value.strip().startswith("/") or not self.suggestions:
|
|
426
|
+
return
|
|
427
|
+
input_widget.value = self.suggestions[0] + " "
|
|
428
|
+
input_widget.cursor_position = len(input_widget.value)
|
|
429
|
+
self.hide_palette()
|
|
430
|
+
|
|
431
|
+
async def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
432
|
+
if event.input.id and str(event.input.id).startswith("setup-"):
|
|
433
|
+
self.action_save_setup()
|
|
434
|
+
return
|
|
435
|
+
text = event.value.strip()
|
|
436
|
+
event.input.value = ""
|
|
437
|
+
self.hide_palette()
|
|
438
|
+
if not text:
|
|
439
|
+
return
|
|
440
|
+
if text in {"0", "q", "quit", "exit", "/exit", "/quit"}:
|
|
441
|
+
self.exit()
|
|
442
|
+
return
|
|
443
|
+
if text in BUTTONS:
|
|
444
|
+
text = BUTTONS[text]
|
|
445
|
+
self.chat.write(f"user > {text}")
|
|
446
|
+
if text == "/clear":
|
|
447
|
+
self.chat.clear()
|
|
448
|
+
self.chat.write("system > Chat cleared.")
|
|
449
|
+
return
|
|
450
|
+
if self.busy:
|
|
451
|
+
self.chat.write("agentram > busy, wait current task")
|
|
452
|
+
return
|
|
453
|
+
self.busy = True
|
|
454
|
+
self.chat.write("agentram > thinking...")
|
|
455
|
+
try:
|
|
456
|
+
response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
|
|
457
|
+
except Exception as error: # noqa: BLE001 - TUI boundary
|
|
458
|
+
response = f"error: {error}"
|
|
459
|
+
finally:
|
|
460
|
+
self.busy = False
|
|
461
|
+
if response == "__clear__":
|
|
462
|
+
self.chat.clear()
|
|
463
|
+
self.chat.write("system > Chat cleared.")
|
|
464
|
+
elif response:
|
|
465
|
+
self.chat.write("agentram > " + response)
|
|
466
|
+
self.refresh_sidebar()
|
|
467
|
+
|
|
468
|
+
AgentRAMTextualApp(context).run()
|
|
469
|
+
return True
|
|
470
|
+
|
|
471
|
+
def run_with_activity(context: McpContext, messages: list[str], user_input: str) -> str:
|
|
472
|
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
473
|
+
future = executor.submit(handle_chat_input, context, user_input, messages)
|
|
474
|
+
frames = ["thinking", "thinking.", "thinking..", "thinking...", "acting", "acting.", "acting..", "acting..."]
|
|
475
|
+
index = 0
|
|
476
|
+
while not future.done():
|
|
477
|
+
draw_screen(context, messages, activity=f"agentram > {frames[index % len(frames)]}")
|
|
478
|
+
index += 1
|
|
479
|
+
time.sleep(0.18)
|
|
480
|
+
return future.result()
|
|
367
481
|
|
|
368
482
|
|
|
369
483
|
|
|
@@ -401,24 +515,24 @@ def read_tui_input(prompt_session: Any | None) -> str:
|
|
|
401
515
|
return input("You > ")
|
|
402
516
|
return prompt_session.prompt("You > ")
|
|
403
517
|
|
|
404
|
-
def handle_chat_input(context: McpContext, text: str, messages: list[str] | None = None) -> str:
|
|
518
|
+
def handle_chat_input(context: McpContext, text: str, messages: list[str] | None = None) -> str:
|
|
405
519
|
if not text:
|
|
406
520
|
return SLASH_HELP
|
|
407
521
|
if text == "/":
|
|
408
522
|
return slash_palette_text()
|
|
409
523
|
if text.startswith("/"):
|
|
410
524
|
return handle_slash_command(context, text, messages)
|
|
411
|
-
call_tool(
|
|
412
|
-
context,
|
|
413
|
-
"agentram_emit_event",
|
|
414
|
-
{"type": "USER_MESSAGE", "payload": {"content": text}, "task_id": "codex", "ingest": False},
|
|
415
|
-
)
|
|
416
|
-
if context.profile_store.list_agents():
|
|
417
|
-
return supervisor_workflow_text(context, text)
|
|
418
|
-
if context.profile_store.auto_route_enabled():
|
|
419
|
-
return route_plain_prompt_text(context, text)
|
|
420
|
-
context_block = retrieve_text(context, text, task_id="codex", limit=6)
|
|
421
|
-
return "Coding chat captured. Relevant context:\n" + context_block
|
|
525
|
+
call_tool(
|
|
526
|
+
context,
|
|
527
|
+
"agentram_emit_event",
|
|
528
|
+
{"type": "USER_MESSAGE", "payload": {"content": text}, "task_id": "codex", "ingest": False},
|
|
529
|
+
)
|
|
530
|
+
if context.profile_store.list_agents():
|
|
531
|
+
return supervisor_workflow_text(context, text)
|
|
532
|
+
if context.profile_store.auto_route_enabled():
|
|
533
|
+
return route_plain_prompt_text(context, text)
|
|
534
|
+
context_block = retrieve_text(context, text, task_id="codex", limit=6)
|
|
535
|
+
return "Coding chat captured. Relevant context:\n" + context_block
|
|
422
536
|
|
|
423
537
|
|
|
424
538
|
def handle_slash_command(context: McpContext, text: str, messages: list[str] | None = None) -> str:
|
|
@@ -455,24 +569,24 @@ def handle_slash_command(context: McpContext, text: str, messages: list[str] | N
|
|
|
455
569
|
return disable_model_text(context, raw_rest)
|
|
456
570
|
if command == "/clear-models":
|
|
457
571
|
return clear_models_text(context)
|
|
458
|
-
if command == "/bind-model":
|
|
459
|
-
if len(rest) < 2:
|
|
460
|
-
return "Usage: /bind-model <provider> <model> [--role coder] [--base-url URL_OR_COMMAND] [--api-key-env ENV_NAME]"
|
|
461
|
-
return bind_model_text(context, parse_bind_model_args(rest))
|
|
462
|
-
if command == "/bind-router":
|
|
463
|
-
if len(rest) < 2:
|
|
464
|
-
return "Usage: /bind-router <provider> <model> [--base-url URL_OR_COMMAND] [--api-key-env ENV_NAME]"
|
|
465
|
-
return bind_router_text(context, parse_bind_model_args(rest))
|
|
466
|
-
if command == "/bind-agent":
|
|
467
|
-
if len(rest) < 3:
|
|
468
|
-
return "Usage: /bind-agent <supervisor|main|small> <provider> <model> [--base-url URL_OR_COMMAND] [--api-key-env ENV_NAME]"
|
|
469
|
-
options = parse_bind_model_args(rest[1:])
|
|
470
|
-
options["slot"] = rest[0]
|
|
471
|
-
return bind_agent_text(context, options)
|
|
472
|
-
if command == "/workflow":
|
|
473
|
-
return workflow_text(context)
|
|
474
|
-
if command == "/route":
|
|
475
|
-
return route_text(context, rest[0] if rest else "status")
|
|
572
|
+
if command == "/bind-model":
|
|
573
|
+
if len(rest) < 2:
|
|
574
|
+
return "Usage: /bind-model <provider> <model> [--role coder] [--base-url URL_OR_COMMAND] [--api-key-env ENV_NAME]"
|
|
575
|
+
return bind_model_text(context, parse_bind_model_args(rest))
|
|
576
|
+
if command == "/bind-router":
|
|
577
|
+
if len(rest) < 2:
|
|
578
|
+
return "Usage: /bind-router <provider> <model> [--base-url URL_OR_COMMAND] [--api-key-env ENV_NAME]"
|
|
579
|
+
return bind_router_text(context, parse_bind_model_args(rest))
|
|
580
|
+
if command == "/bind-agent":
|
|
581
|
+
if len(rest) < 3:
|
|
582
|
+
return "Usage: /bind-agent <supervisor|main|small> <provider> <model> [--base-url URL_OR_COMMAND] [--api-key-env ENV_NAME]"
|
|
583
|
+
options = parse_bind_model_args(rest[1:])
|
|
584
|
+
options["slot"] = rest[0]
|
|
585
|
+
return bind_agent_text(context, options)
|
|
586
|
+
if command == "/workflow":
|
|
587
|
+
return workflow_text(context)
|
|
588
|
+
if command == "/route":
|
|
589
|
+
return route_text(context, rest[0] if rest else "status")
|
|
476
590
|
if command == "/orchestrate":
|
|
477
591
|
execute = "--execute" in rest
|
|
478
592
|
task = raw_rest.replace("--execute", "").strip()
|
|
@@ -626,7 +740,7 @@ def models_text(context: McpContext) -> str:
|
|
|
626
740
|
data = payload(call_tool(context, "agentram_read_coding_models", {}))
|
|
627
741
|
endpoints = data.get("endpoints", [])
|
|
628
742
|
if not endpoints:
|
|
629
|
-
return "No coding models bound. Use /bind-model claude-subagent agentram-reviewer role=reviewer or /bind-model claude claude-3-5-sonnet-latest role=reviewer api_key_env=ANTHROPIC_API_KEY"
|
|
743
|
+
return "No coding models bound. Use /bind-model claude-subagent agentram-reviewer role=reviewer or /bind-model claude claude-3-5-sonnet-latest role=reviewer api_key_env=ANTHROPIC_API_KEY"
|
|
630
744
|
lines = ["Bound coding models:"]
|
|
631
745
|
for endpoint in endpoints:
|
|
632
746
|
lines.append(f"- {endpoint.get('id')} provider={endpoint.get('provider')} model={endpoint.get('model')} role={endpoint.get('role')} modalities={','.join(endpoint.get('modalities', []))}")
|
|
@@ -698,211 +812,211 @@ def bind_model_text(context: McpContext, options: dict[str, object]) -> str:
|
|
|
698
812
|
return f"Bound model: {endpoint.get('id')} provider={endpoint.get('provider')} model={endpoint.get('model')} role={endpoint.get('role')}"
|
|
699
813
|
|
|
700
814
|
|
|
701
|
-
def bind_agent_text(context: McpContext, options: dict[str, object]) -> str:
|
|
702
|
-
slot = str(options.get("slot", "")).strip().lower()
|
|
703
|
-
if slot not in {"supervisor", "main", "small"}:
|
|
704
|
-
return "Usage: /bind-agent <supervisor|main|small> <provider> <model>"
|
|
705
|
-
api_key_env = str(options.get("api_key_env", ""))
|
|
706
|
-
if api_key_env.startswith(("sk-", "gsk_", "sk_")):
|
|
707
|
-
return "Do not paste raw API key into api_key_env. Set env var first, then use api_key_env=ENV_NAME."
|
|
708
|
-
role = {"supervisor": "supervisor", "main": "coder", "small": "memory"}[slot]
|
|
709
|
-
endpoint = AgentModelEndpoint(
|
|
710
|
-
provider=str(options.get("provider", "openai-compatible")),
|
|
711
|
-
model=str(options.get("model", "")),
|
|
712
|
-
role=role,
|
|
713
|
-
base_url=str(options.get("base_url", "")),
|
|
714
|
-
api_key_env=api_key_env,
|
|
715
|
-
modalities=["text"],
|
|
716
|
-
)
|
|
717
|
-
context.profile_store.set_agent(slot, endpoint)
|
|
718
|
-
return f"Bound {slot} agent: provider={endpoint.provider} model={endpoint.model} role={endpoint.role}"
|
|
719
|
-
|
|
720
|
-
def workflow_text(context: McpContext) -> str:
|
|
721
|
-
agents = context.profile_store.list_agents()
|
|
722
|
-
if not agents:
|
|
723
|
-
return "No supervisor workflow agents bound. Use /bind-agent supervisor|main|small <provider> <model>."
|
|
724
|
-
lines = ["Supervisor workflow agents:"]
|
|
725
|
-
for slot in ["supervisor", "main", "small"]:
|
|
726
|
-
endpoint = agents.get(slot)
|
|
727
|
-
if endpoint:
|
|
728
|
-
lines.append(f"- {slot}: {endpoint.provider}:{endpoint.model} role={endpoint.role}")
|
|
729
|
-
else:
|
|
730
|
-
lines.append(f"- {slot}: not bound")
|
|
731
|
-
return "\n".join(lines)
|
|
732
|
-
|
|
733
|
-
def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
|
|
734
|
-
agents = context.profile_store.list_agents()
|
|
735
|
-
supervisor = agents.get("supervisor")
|
|
736
|
-
main_agent = agents.get("main")
|
|
737
|
-
small_agent = agents.get("small")
|
|
738
|
-
memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
|
|
739
|
-
plan = supervisor_plan(supervisor, prompt, memory_context)
|
|
740
|
-
outputs: dict[str, str] = {}
|
|
741
|
-
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
742
|
-
futures = {}
|
|
743
|
-
if main_agent:
|
|
744
|
-
futures[executor.submit(call_agent_endpoint, main_agent, build_main_agent_prompt(prompt, plan, memory_context))] = "main"
|
|
745
|
-
if small_agent:
|
|
746
|
-
futures[executor.submit(call_agent_endpoint, small_agent, build_small_agent_prompt(prompt, plan, memory_context))] = "small"
|
|
747
|
-
for future, slot in list(futures.items()):
|
|
748
|
-
try:
|
|
749
|
-
outputs[slot] = future.result()
|
|
750
|
-
except Exception as error: # noqa: BLE001 - agent boundary
|
|
751
|
-
outputs[slot] = f"error: {model_error_hint(str(error))}"
|
|
752
|
-
if not main_agent:
|
|
753
|
-
outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
|
|
754
|
-
persist_workflow_memory(context, prompt, plan, outputs)
|
|
755
|
-
return "\n".join(
|
|
756
|
-
[
|
|
757
|
-
"Workflow: supervisor -> main + small -> merge",
|
|
758
|
-
"Supervisor plan:",
|
|
759
|
-
plan,
|
|
760
|
-
"",
|
|
761
|
-
"Main agent result:",
|
|
762
|
-
outputs.get("main", "-"),
|
|
763
|
-
"",
|
|
764
|
-
"Small memory agent notes:",
|
|
765
|
-
outputs.get("small", "No small agent bound."),
|
|
766
|
-
]
|
|
767
|
-
)
|
|
768
|
-
|
|
769
|
-
def persist_workflow_memory(context: McpContext, prompt: str, plan: str, outputs: dict[str, str]) -> None:
|
|
770
|
-
small_notes = outputs.get("small", "").strip()
|
|
771
|
-
main_result = outputs.get("main", "").strip()
|
|
772
|
-
note_parts = [
|
|
773
|
-
f"User request: {prompt}",
|
|
774
|
-
f"Supervisor plan: {trim(plan.strip(), 1200)}" if plan.strip() else "Supervisor plan: -",
|
|
775
|
-
]
|
|
776
|
-
if small_notes and not small_notes.startswith("error:"):
|
|
777
|
-
note_parts.append(f"Small memory notes: {trim(small_notes, 1200)}")
|
|
778
|
-
else:
|
|
779
|
-
note_parts.append("Small memory notes: unavailable; persisted supervisor plan as fallback RAM note.")
|
|
780
|
-
if main_result and not main_result.startswith("error:"):
|
|
781
|
-
note_parts.append(f"Main result summary: {trim(main_result, 600)}")
|
|
782
|
-
content = "\n".join(note_parts).strip()
|
|
783
|
-
if not content:
|
|
784
|
-
return
|
|
785
|
-
call_tool(
|
|
786
|
-
context,
|
|
787
|
-
"agentram_emit_event",
|
|
788
|
-
{"type": "DECISION", "payload": {"content": content}, "task_id": "codex", "ingest": True},
|
|
789
|
-
)
|
|
790
|
-
|
|
791
|
-
def supervisor_plan(supervisor: AgentModelEndpoint | None, prompt: str, memory_context: str) -> str:
|
|
792
|
-
if not supervisor:
|
|
793
|
-
return "No supervisor bound. Fallback plan: handle user request, run main coding agent, ask small agent to update RAM notes."
|
|
794
|
-
try:
|
|
795
|
-
return call_agent_endpoint(supervisor, build_supervisor_prompt(prompt, memory_context))
|
|
796
|
-
except Exception as error: # noqa: BLE001 - supervisor boundary
|
|
797
|
-
return f"Supervisor error: {error}\nFallback plan: handle user request, run main coding agent, ask small agent to update RAM notes."
|
|
798
|
-
|
|
799
|
-
def call_agent_endpoint(endpoint: AgentModelEndpoint, prompt: str) -> str:
|
|
800
|
-
return MultiModelCodingOrchestrator([endpoint])._call_endpoint(endpoint, prompt)
|
|
801
|
-
|
|
802
|
-
def build_supervisor_prompt(prompt: str, memory_context: str) -> str:
|
|
803
|
-
return "\n".join(["You are AgentRAM supervisor. Act like a senior coding-agent boss.", "Create concise plan for main coding agent and small memory agent.", "Return only actionable plan. No markdown table.", "User request:", prompt, "AgentRAM context:", memory_context])
|
|
804
|
-
|
|
805
|
-
def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
|
|
806
|
-
return "\n".join(["You are AgentRAM main coding agent.", "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])
|
|
807
|
-
|
|
808
|
-
def build_small_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
|
|
809
|
-
return "\n".join(["You are AgentRAM small memory agent.", "Do not edit code. Note plans, decisions, risks, files, next tasks, and coding facts for RAM.", "Return concise memory notes only.", "User request:", prompt, "Supervisor plan:", plan, "AgentRAM context:", memory_context])
|
|
810
|
-
|
|
811
|
-
def bind_router_text(context: McpContext, options: dict[str, object]) -> str:
|
|
812
|
-
api_key_env = str(options.get("api_key_env", ""))
|
|
813
|
-
if api_key_env.startswith(("sk-", "gsk_", "sk_")):
|
|
814
|
-
return "Do not paste raw API key into api_key_env. Set env var first, then use api_key_env=ENV_NAME."
|
|
815
|
-
endpoint = AgentModelEndpoint(
|
|
816
|
-
provider=str(options.get("provider", "openai-compatible")),
|
|
817
|
-
model=str(options.get("model", "")),
|
|
818
|
-
role="router",
|
|
819
|
-
base_url=str(options.get("base_url", "")),
|
|
820
|
-
api_key_env=api_key_env,
|
|
821
|
-
modalities=["text"],
|
|
822
|
-
)
|
|
823
|
-
context.profile_store.set_router(endpoint)
|
|
824
|
-
return f"Bound router: provider={endpoint.provider} model={endpoint.model}"
|
|
825
|
-
|
|
826
|
-
def route_text(context: McpContext, mode: str) -> str:
|
|
827
|
-
normalized = mode.strip().lower() or "status"
|
|
828
|
-
if normalized == "on":
|
|
829
|
-
context.profile_store.set_auto_route(True)
|
|
830
|
-
elif normalized == "off":
|
|
831
|
-
context.profile_store.set_auto_route(False)
|
|
832
|
-
elif normalized != "status":
|
|
833
|
-
return "Usage: /route on|off|status"
|
|
834
|
-
router = context.profile_store.get_router()
|
|
835
|
-
return "\n".join(
|
|
836
|
-
[
|
|
837
|
-
f"Auto route: {'on' if context.profile_store.auto_route_enabled() else 'off'}",
|
|
838
|
-
f"Router model: {router.provider}:{router.model}" if router else "Router model: heuristic fallback",
|
|
839
|
-
]
|
|
840
|
-
)
|
|
841
|
-
|
|
842
|
-
def route_plain_prompt_text(context: McpContext, prompt: str) -> str:
|
|
843
|
-
decision = classify_prompt_intent(context, prompt)
|
|
844
|
-
intent = str(decision.get("intent", "chat"))
|
|
845
|
-
confidence = float(decision.get("confidence", 0.0))
|
|
846
|
-
prefix = f"Route: {intent} confidence={confidence:.2f}"
|
|
847
|
-
if intent == "coding":
|
|
848
|
-
if context.profile_store.list_endpoints():
|
|
849
|
-
return prefix + "\n" + orchestrate_text(context, prompt, execute=True)
|
|
850
|
-
context_block = retrieve_text(context, prompt, task_id="codex", limit=6)
|
|
851
|
-
return prefix + "\nNo coding models bound. Use /bind-model.\nCoding chat captured. Relevant context:\n" + context_block
|
|
852
|
-
if intent == "planning":
|
|
853
|
-
return prefix + "\n" + drift_text(context, prompt) + "\n\n" + goal_text(context)
|
|
854
|
-
if intent == "memory":
|
|
855
|
-
return prefix + "\n" + note_text(context, prompt)
|
|
856
|
-
context_block = retrieve_text(context, prompt, task_id="codex", limit=6)
|
|
857
|
-
return prefix + "\nCoding chat captured. Relevant context:\n" + context_block
|
|
858
|
-
|
|
859
|
-
def classify_prompt_intent(context: McpContext, prompt: str) -> dict[str, object]:
|
|
860
|
-
router = context.profile_store.get_router()
|
|
861
|
-
if router:
|
|
862
|
-
try:
|
|
863
|
-
response = MultiModelCodingOrchestrator([router])._call_endpoint(router, build_router_prompt(prompt))
|
|
864
|
-
parsed = json.loads(extract_json_object(response))
|
|
865
|
-
if isinstance(parsed, dict) and parsed.get("intent") in {"coding", "planning", "memory", "chat"}:
|
|
866
|
-
return {"intent": parsed.get("intent"), "confidence": float(parsed.get("confidence", 0.0)), "source": "model"}
|
|
867
|
-
except Exception as error: # noqa: BLE001 - router fallback boundary
|
|
868
|
-
return {**heuristic_intent(prompt), "source": "heuristic", "router_error": str(error)}
|
|
869
|
-
return {**heuristic_intent(prompt), "source": "heuristic"}
|
|
870
|
-
|
|
871
|
-
def build_router_prompt(prompt: str) -> str:
|
|
872
|
-
return "\n".join(
|
|
873
|
-
[
|
|
874
|
-
"Classify user prompt for AgentRAM routing.",
|
|
875
|
-
"Return only JSON, no markdown.",
|
|
876
|
-
"Schema: {\"intent\":\"coding|planning|memory|chat\",\"confidence\":0.0,\"reason\":\"short\"}",
|
|
877
|
-
"coding = edit/review/debug/refactor/test/code/file requests.",
|
|
878
|
-
"planning = goal stack, milestone, mission, next task, drift, roadmap.",
|
|
879
|
-
"memory = remember/note/decision/fact to save.",
|
|
880
|
-
"chat = other normal conversation.",
|
|
881
|
-
f"Prompt: {prompt}",
|
|
882
|
-
]
|
|
883
|
-
)
|
|
884
|
-
|
|
885
|
-
def extract_json_object(text: str) -> str:
|
|
886
|
-
start = text.find("{")
|
|
887
|
-
end = text.rfind("}")
|
|
888
|
-
if start == -1 or end == -1 or end < start:
|
|
889
|
-
return "{}"
|
|
890
|
-
return text[start : end + 1]
|
|
891
|
-
|
|
892
|
-
def heuristic_intent(prompt: str) -> dict[str, object]:
|
|
893
|
-
lower = prompt.lower()
|
|
894
|
-
memory_words = ["ghi nhớ", "remember", "note", "decision", "lưu lại", "save this"]
|
|
895
|
-
planning_words = ["goal", "kế hoạch", "ke hoach", "milestone", "mission", "drift", "next task", "roadmap", "mục tiêu", "muc tieu"]
|
|
896
|
-
coding_words = ["sửa", "sua", "fix", "bug", "code", "test", "refactor", "review", "file", ".py", ".js", "endpoint", "class", "function"]
|
|
897
|
-
if any(word in lower for word in memory_words):
|
|
898
|
-
return {"intent": "memory", "confidence": 0.75}
|
|
899
|
-
if any(word in lower for word in planning_words):
|
|
900
|
-
return {"intent": "planning", "confidence": 0.75}
|
|
901
|
-
if any(word in lower for word in coding_words):
|
|
902
|
-
return {"intent": "coding", "confidence": 0.72}
|
|
903
|
-
return {"intent": "chat", "confidence": 0.55}
|
|
904
|
-
|
|
905
|
-
def orchestrate_text(context: McpContext, task: str, files: list[str] | None = None, execute: bool = False) -> str:
|
|
815
|
+
def bind_agent_text(context: McpContext, options: dict[str, object]) -> str:
|
|
816
|
+
slot = str(options.get("slot", "")).strip().lower()
|
|
817
|
+
if slot not in {"supervisor", "main", "small"}:
|
|
818
|
+
return "Usage: /bind-agent <supervisor|main|small> <provider> <model>"
|
|
819
|
+
api_key_env = str(options.get("api_key_env", ""))
|
|
820
|
+
if api_key_env.startswith(("sk-", "gsk_", "sk_")):
|
|
821
|
+
return "Do not paste raw API key into api_key_env. Set env var first, then use api_key_env=ENV_NAME."
|
|
822
|
+
role = {"supervisor": "supervisor", "main": "coder", "small": "memory"}[slot]
|
|
823
|
+
endpoint = AgentModelEndpoint(
|
|
824
|
+
provider=str(options.get("provider", "openai-compatible")),
|
|
825
|
+
model=str(options.get("model", "")),
|
|
826
|
+
role=role,
|
|
827
|
+
base_url=str(options.get("base_url", "")),
|
|
828
|
+
api_key_env=api_key_env,
|
|
829
|
+
modalities=["text"],
|
|
830
|
+
)
|
|
831
|
+
context.profile_store.set_agent(slot, endpoint)
|
|
832
|
+
return f"Bound {slot} agent: provider={endpoint.provider} model={endpoint.model} role={endpoint.role}"
|
|
833
|
+
|
|
834
|
+
def workflow_text(context: McpContext) -> str:
|
|
835
|
+
agents = context.profile_store.list_agents()
|
|
836
|
+
if not agents:
|
|
837
|
+
return "No supervisor workflow agents bound. Use /bind-agent supervisor|main|small <provider> <model>."
|
|
838
|
+
lines = ["Supervisor workflow agents:"]
|
|
839
|
+
for slot in ["supervisor", "main", "small"]:
|
|
840
|
+
endpoint = agents.get(slot)
|
|
841
|
+
if endpoint:
|
|
842
|
+
lines.append(f"- {slot}: {endpoint.provider}:{endpoint.model} role={endpoint.role}")
|
|
843
|
+
else:
|
|
844
|
+
lines.append(f"- {slot}: not bound")
|
|
845
|
+
return "\n".join(lines)
|
|
846
|
+
|
|
847
|
+
def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
|
|
848
|
+
agents = context.profile_store.list_agents()
|
|
849
|
+
supervisor = agents.get("supervisor")
|
|
850
|
+
main_agent = agents.get("main")
|
|
851
|
+
small_agent = agents.get("small")
|
|
852
|
+
memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
|
|
853
|
+
plan = supervisor_plan(supervisor, prompt, memory_context)
|
|
854
|
+
outputs: dict[str, str] = {}
|
|
855
|
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
856
|
+
futures = {}
|
|
857
|
+
if main_agent:
|
|
858
|
+
futures[executor.submit(call_agent_endpoint, main_agent, build_main_agent_prompt(prompt, plan, memory_context))] = "main"
|
|
859
|
+
if small_agent:
|
|
860
|
+
futures[executor.submit(call_agent_endpoint, small_agent, build_small_agent_prompt(prompt, plan, memory_context))] = "small"
|
|
861
|
+
for future, slot in list(futures.items()):
|
|
862
|
+
try:
|
|
863
|
+
outputs[slot] = future.result()
|
|
864
|
+
except Exception as error: # noqa: BLE001 - agent boundary
|
|
865
|
+
outputs[slot] = f"error: {model_error_hint(str(error))}"
|
|
866
|
+
if not main_agent:
|
|
867
|
+
outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
|
|
868
|
+
persist_workflow_memory(context, prompt, plan, outputs)
|
|
869
|
+
return "\n".join(
|
|
870
|
+
[
|
|
871
|
+
"Workflow: supervisor -> main + small -> merge",
|
|
872
|
+
"Supervisor plan:",
|
|
873
|
+
plan,
|
|
874
|
+
"",
|
|
875
|
+
"Main agent result:",
|
|
876
|
+
outputs.get("main", "-"),
|
|
877
|
+
"",
|
|
878
|
+
"Small memory agent notes:",
|
|
879
|
+
outputs.get("small", "No small agent bound."),
|
|
880
|
+
]
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
def persist_workflow_memory(context: McpContext, prompt: str, plan: str, outputs: dict[str, str]) -> None:
|
|
884
|
+
small_notes = outputs.get("small", "").strip()
|
|
885
|
+
main_result = outputs.get("main", "").strip()
|
|
886
|
+
note_parts = [
|
|
887
|
+
f"User request: {prompt}",
|
|
888
|
+
f"Supervisor plan: {trim(plan.strip(), 1200)}" if plan.strip() else "Supervisor plan: -",
|
|
889
|
+
]
|
|
890
|
+
if small_notes and not small_notes.startswith("error:"):
|
|
891
|
+
note_parts.append(f"Small memory notes: {trim(small_notes, 1200)}")
|
|
892
|
+
else:
|
|
893
|
+
note_parts.append("Small memory notes: unavailable; persisted supervisor plan as fallback RAM note.")
|
|
894
|
+
if main_result and not main_result.startswith("error:"):
|
|
895
|
+
note_parts.append(f"Main result summary: {trim(main_result, 600)}")
|
|
896
|
+
content = "\n".join(note_parts).strip()
|
|
897
|
+
if not content:
|
|
898
|
+
return
|
|
899
|
+
call_tool(
|
|
900
|
+
context,
|
|
901
|
+
"agentram_emit_event",
|
|
902
|
+
{"type": "DECISION", "payload": {"content": content}, "task_id": "codex", "ingest": True},
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
def supervisor_plan(supervisor: AgentModelEndpoint | None, prompt: str, memory_context: str) -> str:
|
|
906
|
+
if not supervisor:
|
|
907
|
+
return "No supervisor bound. Fallback plan: handle user request, run main coding agent, ask small agent to update RAM notes."
|
|
908
|
+
try:
|
|
909
|
+
return call_agent_endpoint(supervisor, build_supervisor_prompt(prompt, memory_context))
|
|
910
|
+
except Exception as error: # noqa: BLE001 - supervisor boundary
|
|
911
|
+
return f"Supervisor error: {error}\nFallback plan: handle user request, run main coding agent, ask small agent to update RAM notes."
|
|
912
|
+
|
|
913
|
+
def call_agent_endpoint(endpoint: AgentModelEndpoint, prompt: str) -> str:
|
|
914
|
+
return MultiModelCodingOrchestrator([endpoint])._call_endpoint(endpoint, prompt)
|
|
915
|
+
|
|
916
|
+
def build_supervisor_prompt(prompt: str, memory_context: str) -> str:
|
|
917
|
+
return "\n".join(["You are AgentRAM supervisor. Act like a senior coding-agent boss.", "Create concise plan for main coding agent and small memory agent.", "Return only actionable plan. No markdown table.", "User request:", prompt, "AgentRAM context:", memory_context])
|
|
918
|
+
|
|
919
|
+
def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
|
|
920
|
+
return "\n".join(["You are AgentRAM main coding agent.", "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])
|
|
921
|
+
|
|
922
|
+
def build_small_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
|
|
923
|
+
return "\n".join(["You are AgentRAM small memory agent.", "Do not edit code. Note plans, decisions, risks, files, next tasks, and coding facts for RAM.", "Return concise memory notes only.", "User request:", prompt, "Supervisor plan:", plan, "AgentRAM context:", memory_context])
|
|
924
|
+
|
|
925
|
+
def bind_router_text(context: McpContext, options: dict[str, object]) -> str:
|
|
926
|
+
api_key_env = str(options.get("api_key_env", ""))
|
|
927
|
+
if api_key_env.startswith(("sk-", "gsk_", "sk_")):
|
|
928
|
+
return "Do not paste raw API key into api_key_env. Set env var first, then use api_key_env=ENV_NAME."
|
|
929
|
+
endpoint = AgentModelEndpoint(
|
|
930
|
+
provider=str(options.get("provider", "openai-compatible")),
|
|
931
|
+
model=str(options.get("model", "")),
|
|
932
|
+
role="router",
|
|
933
|
+
base_url=str(options.get("base_url", "")),
|
|
934
|
+
api_key_env=api_key_env,
|
|
935
|
+
modalities=["text"],
|
|
936
|
+
)
|
|
937
|
+
context.profile_store.set_router(endpoint)
|
|
938
|
+
return f"Bound router: provider={endpoint.provider} model={endpoint.model}"
|
|
939
|
+
|
|
940
|
+
def route_text(context: McpContext, mode: str) -> str:
|
|
941
|
+
normalized = mode.strip().lower() or "status"
|
|
942
|
+
if normalized == "on":
|
|
943
|
+
context.profile_store.set_auto_route(True)
|
|
944
|
+
elif normalized == "off":
|
|
945
|
+
context.profile_store.set_auto_route(False)
|
|
946
|
+
elif normalized != "status":
|
|
947
|
+
return "Usage: /route on|off|status"
|
|
948
|
+
router = context.profile_store.get_router()
|
|
949
|
+
return "\n".join(
|
|
950
|
+
[
|
|
951
|
+
f"Auto route: {'on' if context.profile_store.auto_route_enabled() else 'off'}",
|
|
952
|
+
f"Router model: {router.provider}:{router.model}" if router else "Router model: heuristic fallback",
|
|
953
|
+
]
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
def route_plain_prompt_text(context: McpContext, prompt: str) -> str:
|
|
957
|
+
decision = classify_prompt_intent(context, prompt)
|
|
958
|
+
intent = str(decision.get("intent", "chat"))
|
|
959
|
+
confidence = float(decision.get("confidence", 0.0))
|
|
960
|
+
prefix = f"Route: {intent} confidence={confidence:.2f}"
|
|
961
|
+
if intent == "coding":
|
|
962
|
+
if context.profile_store.list_endpoints():
|
|
963
|
+
return prefix + "\n" + orchestrate_text(context, prompt, execute=True)
|
|
964
|
+
context_block = retrieve_text(context, prompt, task_id="codex", limit=6)
|
|
965
|
+
return prefix + "\nNo coding models bound. Use /bind-model.\nCoding chat captured. Relevant context:\n" + context_block
|
|
966
|
+
if intent == "planning":
|
|
967
|
+
return prefix + "\n" + drift_text(context, prompt) + "\n\n" + goal_text(context)
|
|
968
|
+
if intent == "memory":
|
|
969
|
+
return prefix + "\n" + note_text(context, prompt)
|
|
970
|
+
context_block = retrieve_text(context, prompt, task_id="codex", limit=6)
|
|
971
|
+
return prefix + "\nCoding chat captured. Relevant context:\n" + context_block
|
|
972
|
+
|
|
973
|
+
def classify_prompt_intent(context: McpContext, prompt: str) -> dict[str, object]:
|
|
974
|
+
router = context.profile_store.get_router()
|
|
975
|
+
if router:
|
|
976
|
+
try:
|
|
977
|
+
response = MultiModelCodingOrchestrator([router])._call_endpoint(router, build_router_prompt(prompt))
|
|
978
|
+
parsed = json.loads(extract_json_object(response))
|
|
979
|
+
if isinstance(parsed, dict) and parsed.get("intent") in {"coding", "planning", "memory", "chat"}:
|
|
980
|
+
return {"intent": parsed.get("intent"), "confidence": float(parsed.get("confidence", 0.0)), "source": "model"}
|
|
981
|
+
except Exception as error: # noqa: BLE001 - router fallback boundary
|
|
982
|
+
return {**heuristic_intent(prompt), "source": "heuristic", "router_error": str(error)}
|
|
983
|
+
return {**heuristic_intent(prompt), "source": "heuristic"}
|
|
984
|
+
|
|
985
|
+
def build_router_prompt(prompt: str) -> str:
|
|
986
|
+
return "\n".join(
|
|
987
|
+
[
|
|
988
|
+
"Classify user prompt for AgentRAM routing.",
|
|
989
|
+
"Return only JSON, no markdown.",
|
|
990
|
+
"Schema: {\"intent\":\"coding|planning|memory|chat\",\"confidence\":0.0,\"reason\":\"short\"}",
|
|
991
|
+
"coding = edit/review/debug/refactor/test/code/file requests.",
|
|
992
|
+
"planning = goal stack, milestone, mission, next task, drift, roadmap.",
|
|
993
|
+
"memory = remember/note/decision/fact to save.",
|
|
994
|
+
"chat = other normal conversation.",
|
|
995
|
+
f"Prompt: {prompt}",
|
|
996
|
+
]
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
def extract_json_object(text: str) -> str:
|
|
1000
|
+
start = text.find("{")
|
|
1001
|
+
end = text.rfind("}")
|
|
1002
|
+
if start == -1 or end == -1 or end < start:
|
|
1003
|
+
return "{}"
|
|
1004
|
+
return text[start : end + 1]
|
|
1005
|
+
|
|
1006
|
+
def heuristic_intent(prompt: str) -> dict[str, object]:
|
|
1007
|
+
lower = prompt.lower()
|
|
1008
|
+
memory_words = ["ghi nhớ", "remember", "note", "decision", "lưu lại", "save this"]
|
|
1009
|
+
planning_words = ["goal", "kế hoạch", "ke hoach", "milestone", "mission", "drift", "next task", "roadmap", "mục tiêu", "muc tieu"]
|
|
1010
|
+
coding_words = ["sửa", "sua", "fix", "bug", "code", "test", "refactor", "review", "file", ".py", ".js", "endpoint", "class", "function"]
|
|
1011
|
+
if any(word in lower for word in memory_words):
|
|
1012
|
+
return {"intent": "memory", "confidence": 0.75}
|
|
1013
|
+
if any(word in lower for word in planning_words):
|
|
1014
|
+
return {"intent": "planning", "confidence": 0.75}
|
|
1015
|
+
if any(word in lower for word in coding_words):
|
|
1016
|
+
return {"intent": "coding", "confidence": 0.72}
|
|
1017
|
+
return {"intent": "chat", "confidence": 0.55}
|
|
1018
|
+
|
|
1019
|
+
def orchestrate_text(context: McpContext, task: str, files: list[str] | None = None, execute: bool = False) -> str:
|
|
906
1020
|
if not task.strip():
|
|
907
1021
|
return "Usage: /orchestrate <coding task>"
|
|
908
1022
|
tool = "agentram_run_coding_orchestration" if execute else "agentram_build_coding_orchestration"
|
|
@@ -926,16 +1040,16 @@ def model_error_hint(message: str) -> str:
|
|
|
926
1040
|
lower = message.lower()
|
|
927
1041
|
if "base_url is required" in lower:
|
|
928
1042
|
return message + " | Fix: /remove-model <id> or re-bind with --base-url http://localhost:8000/v1"
|
|
929
|
-
if "actively refused" in lower or "connection refused" in lower or "winerror 10061" in lower:
|
|
930
|
-
return message + " | Fix: start your OpenAI-compatible server at base_url, or change --base-url."
|
|
931
|
-
if "http 404" in lower or "404: not found" in lower:
|
|
932
|
-
return message + " | Fix: verify base_url includes /v1, provider supports /chat/completions, and model name exists."
|
|
933
|
-
if "http 400" in lower and "not supported" in lower:
|
|
934
|
-
return message + " | Fix: this account/provider rejects that model. Bind a supported model name from your local endpoint."
|
|
935
|
-
if "returned non-json" in lower or "expecting value" in lower:
|
|
936
|
-
return message + " | Fix: endpoint returned empty/HTML/non-JSON. Check base_url, auth key, proxy, and server logs."
|
|
937
|
-
if "missing api key env" in lower:
|
|
938
|
-
return message + " | Fix: set environment variable, then bind using --api-key-env ENV_NAME, not raw key."
|
|
1043
|
+
if "actively refused" in lower or "connection refused" in lower or "winerror 10061" in lower:
|
|
1044
|
+
return message + " | Fix: start your OpenAI-compatible server at base_url, or change --base-url."
|
|
1045
|
+
if "http 404" in lower or "404: not found" in lower:
|
|
1046
|
+
return message + " | Fix: verify base_url includes /v1, provider supports /chat/completions, and model name exists."
|
|
1047
|
+
if "http 400" in lower and "not supported" in lower:
|
|
1048
|
+
return message + " | Fix: this account/provider rejects that model. Bind a supported model name from your local endpoint."
|
|
1049
|
+
if "returned non-json" in lower or "expecting value" in lower:
|
|
1050
|
+
return message + " | Fix: endpoint returned empty/HTML/non-JSON. Check base_url, auth key, proxy, and server logs."
|
|
1051
|
+
if "missing api key env" in lower:
|
|
1052
|
+
return message + " | Fix: set environment variable, then bind using --api-key-env ENV_NAME, not raw key."
|
|
939
1053
|
return message
|
|
940
1054
|
|
|
941
1055
|
def claude_template_root() -> Path:
|
|
@@ -1004,7 +1118,7 @@ BUTTONS = {
|
|
|
1004
1118
|
}
|
|
1005
1119
|
|
|
1006
1120
|
|
|
1007
|
-
def draw_screen(context: McpContext, messages: list[str], activity: str | None = None) -> None:
|
|
1121
|
+
def draw_screen(context: McpContext, messages: list[str], activity: str | None = None) -> None:
|
|
1008
1122
|
clear_terminal()
|
|
1009
1123
|
width = min(max(shutil.get_terminal_size((110, 34)).columns, 86), 132)
|
|
1010
1124
|
left_width = 30
|
|
@@ -1013,17 +1127,17 @@ def draw_screen(context: McpContext, messages: list[str], activity: str | None =
|
|
|
1013
1127
|
print(box_text("◆ AgentRAM Code | RAM + Claude subagents + slash commands", width))
|
|
1014
1128
|
print("+" + "-" * (width - 2) + "+")
|
|
1015
1129
|
|
|
1016
|
-
sidebar = sidebar_lines(context)
|
|
1017
|
-
display_messages = [*messages, activity] if activity else messages
|
|
1018
|
-
chat = wrap_messages(display_messages, right_width - 4)[-22:]
|
|
1130
|
+
sidebar = sidebar_lines(context)
|
|
1131
|
+
display_messages = [*messages, activity] if activity else messages
|
|
1132
|
+
chat = wrap_messages(display_messages, right_width - 4)[-22:]
|
|
1019
1133
|
rows = max(len(sidebar), len(chat), 22)
|
|
1020
1134
|
for index in range(rows):
|
|
1021
1135
|
left = sidebar[index] if index < len(sidebar) else ""
|
|
1022
1136
|
right = chat[index] if index < len(chat) else ""
|
|
1023
1137
|
print("| " + left[: left_width - 2].ljust(left_width - 2) + "| " + right[: right_width - 2].ljust(right_width - 2) + "|")
|
|
1024
1138
|
print("+" + "-" * (left_width - 1) + "+" + "-" * (right_width) + "+")
|
|
1025
|
-
footer = "Thinking/acting..." if activity else "Input: type / for commands. Use mouse wheel or terminal scrollbar for previous output."
|
|
1026
|
-
print(box_text(footer, width))
|
|
1139
|
+
footer = "Thinking/acting..." if activity else "Input: type / for commands. Use mouse wheel or terminal scrollbar for previous output."
|
|
1140
|
+
print(box_text(footer, width))
|
|
1027
1141
|
print("+" + "-" * (width - 2) + "+")
|
|
1028
1142
|
|
|
1029
1143
|
|
|
@@ -1062,8 +1176,8 @@ def sidebar_lines(context: McpContext) -> list[str]:
|
|
|
1062
1176
|
" /ask <prompt>",
|
|
1063
1177
|
]
|
|
1064
1178
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1179
|
+
|
|
1180
|
+
|
|
1067
1181
|
def trim(text: str, limit: int) -> str:
|
|
1068
1182
|
return text if len(text) <= limit else text[: limit - 1] + "…"
|
|
1069
1183
|
|
package/package.json
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "agentram",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Async, model-agnostic Working RAM for coding agents.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"homepage": "https://github.com/trancongnghia/AGENT_RAM#readme",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/trancongnghia/AGENT_RAM.git"
|
|
10
|
-
},
|
|
11
|
-
"bugs": {
|
|
12
|
-
"url": "https://github.com/trancongnghia/AGENT_RAM/issues"
|
|
13
|
-
},
|
|
14
|
-
"keywords": [
|
|
15
|
-
"agent",
|
|
16
|
-
"memory",
|
|
17
|
-
"codex",
|
|
18
|
-
"claude",
|
|
19
|
-
"mcp",
|
|
20
|
-
"llm"
|
|
21
|
-
],
|
|
22
|
-
"bin": {
|
|
23
|
-
"agentram": "bin/agentram.js",
|
|
24
|
-
"agentram-mcp": "bin/agentram-mcp.js"
|
|
25
|
-
},
|
|
26
|
-
"scripts": {
|
|
27
|
-
"agentram": "node bin/agentram.js",
|
|
28
|
-
"agentram:mcp": "node bin/agentram-mcp.js",
|
|
29
|
-
"test": "python -m pytest -q",
|
|
30
|
-
"pack:dry-run": "npm pack --dry-run",
|
|
31
|
-
"prepublishOnly": "npm test",
|
|
32
|
-
"postinstall": "node bin/agentram-postinstall.js"
|
|
33
|
-
},
|
|
34
|
-
"files": [
|
|
35
|
-
"agentram/*.py",
|
|
36
|
-
"bin/*.js",
|
|
37
|
-
".claude/agents/*.md",
|
|
38
|
-
".claude/commands/*.md",
|
|
39
|
-
"agentram_mcp_server.py",
|
|
40
|
-
"agentram_daemon.py",
|
|
41
|
-
"codex_ram.py",
|
|
42
|
-
"pyproject.toml",
|
|
43
|
-
"README.md",
|
|
44
|
-
"LICENSE"
|
|
45
|
-
],
|
|
46
|
-
"engines": {
|
|
47
|
-
"node": ">=16"
|
|
48
|
-
},
|
|
49
|
-
"publishConfig": {
|
|
50
|
-
"access": "public"
|
|
51
|
-
}
|
|
52
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "agentram",
|
|
3
|
+
"version": "0.1.15",
|
|
4
|
+
"description": "Async, model-agnostic Working RAM for coding agents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/trancongnghia/AGENT_RAM#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/trancongnghia/AGENT_RAM.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/trancongnghia/AGENT_RAM/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"agent",
|
|
16
|
+
"memory",
|
|
17
|
+
"codex",
|
|
18
|
+
"claude",
|
|
19
|
+
"mcp",
|
|
20
|
+
"llm"
|
|
21
|
+
],
|
|
22
|
+
"bin": {
|
|
23
|
+
"agentram": "bin/agentram.js",
|
|
24
|
+
"agentram-mcp": "bin/agentram-mcp.js"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"agentram": "node bin/agentram.js",
|
|
28
|
+
"agentram:mcp": "node bin/agentram-mcp.js",
|
|
29
|
+
"test": "python -m pytest -q",
|
|
30
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
31
|
+
"prepublishOnly": "npm test",
|
|
32
|
+
"postinstall": "node bin/agentram-postinstall.js"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"agentram/*.py",
|
|
36
|
+
"bin/*.js",
|
|
37
|
+
".claude/agents/*.md",
|
|
38
|
+
".claude/commands/*.md",
|
|
39
|
+
"agentram_mcp_server.py",
|
|
40
|
+
"agentram_daemon.py",
|
|
41
|
+
"codex_ram.py",
|
|
42
|
+
"pyproject.toml",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=16"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|