agentram 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agentram/cli.py CHANGED
@@ -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,176 @@ 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
- #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
356
-
357
- def run_with_activity(context: McpContext, messages: list[str], user_input: str) -> str:
358
- with ThreadPoolExecutor(max_workers=1) as executor:
359
- future = executor.submit(handle_chat_input, context, user_input, messages)
360
- frames = ["thinking", "thinking.", "thinking..", "thinking...", "acting", "acting.", "acting..", "acting..."]
361
- index = 0
362
- while not future.done():
363
- draw_screen(context, messages, activity=f"agentram > {frames[index % len(frames)]}")
364
- index += 1
365
- time.sleep(0.18)
366
- return future.result()
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
+ #palette { height: auto; max-height: 12; border: solid #3b82f6; padding: 1; background: #101826; display: none; }
288
+ #input { dock: bottom; border: solid #3b82f6; }
289
+ .muted { color: #8b9bb0; }
290
+ """
291
+ BINDINGS = [("ctrl+c", "quit", "Quit"), ("tab", "accept_suggestion", "Complete slash")]
292
+
293
+ def __init__(self, mcp_context: McpContext) -> None:
294
+ super().__init__()
295
+ self.context = mcp_context
296
+ self.messages: list[str] = []
297
+ self.busy = False
298
+ self.suggestions: list[str] = []
299
+
300
+ def compose(self) -> ComposeResult:
301
+ yield Header(show_clock=True)
302
+ with Horizontal(id="layout"):
303
+ yield Static(id="sidebar")
304
+ with Vertical(id="main"):
305
+ yield RichLog(id="chat", wrap=True, highlight=True, markup=False, auto_scroll=True)
306
+ yield Input(placeholder="Prompt hoặc /command. Mouse wheel scroll trong khung chat.", id="input")
307
+ yield Footer()
308
+
309
+ def on_mount(self) -> None:
310
+ self.context.init_storage()
311
+ self.chat.write("system > AgentRAM Textual TUI ready")
312
+ self.chat.write(f"system > RAM root: {self.context.ram_root}")
313
+ self.refresh_sidebar()
314
+ self.set_interval(1.0, self.refresh_sidebar)
315
+
316
+ @property
317
+ def chat(self) -> RichLog:
318
+ return self.query_one("#chat", RichLog)
319
+
320
+ def refresh_sidebar(self) -> None:
321
+ self.query_one("#sidebar", Static).update("\n".join(sidebar_lines(self.context)))
322
+
323
+ def hide_palette(self) -> None:
324
+ palette = self.query_one("#palette", Static)
325
+ palette.update("")
326
+ palette.styles.display = "none"
327
+
328
+ def show_palette(self, value: str) -> None:
329
+ palette = self.query_one("#palette", Static)
330
+ query = value.strip().lower().lstrip("/")
331
+ matches = []
332
+ for command, description in SLASH_COMMANDS:
333
+ if not command.startswith("/"):
334
+ continue
335
+ command_name = command.split()[0]
336
+ haystack = command_name.lower().lstrip("/")
337
+ if query and query not in haystack:
338
+ continue
339
+ matches.append((command_name, description))
340
+ self.suggestions = [command for command, _ in matches[:8]]
341
+ if not matches:
342
+ palette.update("No matching slash command")
343
+ else:
344
+ lines = ["Slash commands ? Tab completes first match"]
345
+ lines.extend(f" {command.ljust(18)} {description}" for command, description in matches[:8])
346
+ palette.update("\n".join(lines))
347
+ palette.styles.display = "block"
348
+
349
+ def on_input_changed(self, event: Input.Changed) -> None:
350
+ value = event.value.strip()
351
+ if value.startswith("/"):
352
+ self.show_palette(value)
353
+ else:
354
+ self.hide_palette()
355
+
356
+ def action_accept_suggestion(self) -> None:
357
+ input_widget = self.query_one("#input", Input)
358
+ if not input_widget.value.strip().startswith("/") or not self.suggestions:
359
+ return
360
+ input_widget.value = self.suggestions[0] + " "
361
+ input_widget.cursor_position = len(input_widget.value)
362
+ self.hide_palette()
363
+
364
+ async def on_input_submitted(self, event: Input.Submitted) -> None:
365
+ text = event.value.strip()
366
+ event.input.value = ""
367
+ self.hide_palette()
368
+ if not text:
369
+ return
370
+ if text in {"0", "q", "quit", "exit", "/exit", "/quit"}:
371
+ self.exit()
372
+ return
373
+ if text in BUTTONS:
374
+ text = BUTTONS[text]
375
+ self.chat.write(f"user > {text}")
376
+ if text == "/clear":
377
+ self.chat.clear()
378
+ self.chat.write("system > Chat cleared.")
379
+ return
380
+ if self.busy:
381
+ self.chat.write("agentram > busy, wait current task")
382
+ return
383
+ self.busy = True
384
+ self.chat.write("agentram > thinking...")
385
+ try:
386
+ response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
387
+ except Exception as error: # noqa: BLE001 - TUI boundary
388
+ response = f"error: {error}"
389
+ finally:
390
+ self.busy = False
391
+ if response == "__clear__":
392
+ self.chat.clear()
393
+ self.chat.write("system > Chat cleared.")
394
+ elif response:
395
+ self.chat.write("agentram > " + response)
396
+ self.refresh_sidebar()
397
+
398
+ AgentRAMTextualApp(context).run()
399
+ return True
400
+
401
+ def run_with_activity(context: McpContext, messages: list[str], user_input: str) -> str:
402
+ with ThreadPoolExecutor(max_workers=1) as executor:
403
+ future = executor.submit(handle_chat_input, context, user_input, messages)
404
+ frames = ["thinking", "thinking.", "thinking..", "thinking...", "acting", "acting.", "acting..", "acting..."]
405
+ index = 0
406
+ while not future.done():
407
+ draw_screen(context, messages, activity=f"agentram > {frames[index % len(frames)]}")
408
+ index += 1
409
+ time.sleep(0.18)
410
+ return future.result()
367
411
 
368
412
 
369
413
 
@@ -401,24 +445,24 @@ def read_tui_input(prompt_session: Any | None) -> str:
401
445
  return input("You > ")
402
446
  return prompt_session.prompt("You > ")
403
447
 
404
- def handle_chat_input(context: McpContext, text: str, messages: list[str] | None = None) -> str:
448
+ def handle_chat_input(context: McpContext, text: str, messages: list[str] | None = None) -> str:
405
449
  if not text:
406
450
  return SLASH_HELP
407
451
  if text == "/":
408
452
  return slash_palette_text()
409
453
  if text.startswith("/"):
410
454
  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
455
+ call_tool(
456
+ context,
457
+ "agentram_emit_event",
458
+ {"type": "USER_MESSAGE", "payload": {"content": text}, "task_id": "codex", "ingest": False},
459
+ )
460
+ if context.profile_store.list_agents():
461
+ return supervisor_workflow_text(context, text)
462
+ if context.profile_store.auto_route_enabled():
463
+ return route_plain_prompt_text(context, text)
464
+ context_block = retrieve_text(context, text, task_id="codex", limit=6)
465
+ return "Coding chat captured. Relevant context:\n" + context_block
422
466
 
423
467
 
424
468
  def handle_slash_command(context: McpContext, text: str, messages: list[str] | None = None) -> str:
@@ -455,24 +499,24 @@ def handle_slash_command(context: McpContext, text: str, messages: list[str] | N
455
499
  return disable_model_text(context, raw_rest)
456
500
  if command == "/clear-models":
457
501
  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")
502
+ if command == "/bind-model":
503
+ if len(rest) < 2:
504
+ return "Usage: /bind-model <provider> <model> [--role coder] [--base-url URL_OR_COMMAND] [--api-key-env ENV_NAME]"
505
+ return bind_model_text(context, parse_bind_model_args(rest))
506
+ if command == "/bind-router":
507
+ if len(rest) < 2:
508
+ return "Usage: /bind-router <provider> <model> [--base-url URL_OR_COMMAND] [--api-key-env ENV_NAME]"
509
+ return bind_router_text(context, parse_bind_model_args(rest))
510
+ if command == "/bind-agent":
511
+ if len(rest) < 3:
512
+ return "Usage: /bind-agent <supervisor|main|small> <provider> <model> [--base-url URL_OR_COMMAND] [--api-key-env ENV_NAME]"
513
+ options = parse_bind_model_args(rest[1:])
514
+ options["slot"] = rest[0]
515
+ return bind_agent_text(context, options)
516
+ if command == "/workflow":
517
+ return workflow_text(context)
518
+ if command == "/route":
519
+ return route_text(context, rest[0] if rest else "status")
476
520
  if command == "/orchestrate":
477
521
  execute = "--execute" in rest
478
522
  task = raw_rest.replace("--execute", "").strip()
@@ -626,7 +670,7 @@ def models_text(context: McpContext) -> str:
626
670
  data = payload(call_tool(context, "agentram_read_coding_models", {}))
627
671
  endpoints = data.get("endpoints", [])
628
672
  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"
673
+ 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
674
  lines = ["Bound coding models:"]
631
675
  for endpoint in endpoints:
632
676
  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 +742,211 @@ def bind_model_text(context: McpContext, options: dict[str, object]) -> str:
698
742
  return f"Bound model: {endpoint.get('id')} provider={endpoint.get('provider')} model={endpoint.get('model')} role={endpoint.get('role')}"
699
743
 
700
744
 
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:
745
+ def bind_agent_text(context: McpContext, options: dict[str, object]) -> str:
746
+ slot = str(options.get("slot", "")).strip().lower()
747
+ if slot not in {"supervisor", "main", "small"}:
748
+ return "Usage: /bind-agent <supervisor|main|small> <provider> <model>"
749
+ api_key_env = str(options.get("api_key_env", ""))
750
+ if api_key_env.startswith(("sk-", "gsk_", "sk_")):
751
+ return "Do not paste raw API key into api_key_env. Set env var first, then use api_key_env=ENV_NAME."
752
+ role = {"supervisor": "supervisor", "main": "coder", "small": "memory"}[slot]
753
+ endpoint = AgentModelEndpoint(
754
+ provider=str(options.get("provider", "openai-compatible")),
755
+ model=str(options.get("model", "")),
756
+ role=role,
757
+ base_url=str(options.get("base_url", "")),
758
+ api_key_env=api_key_env,
759
+ modalities=["text"],
760
+ )
761
+ context.profile_store.set_agent(slot, endpoint)
762
+ return f"Bound {slot} agent: provider={endpoint.provider} model={endpoint.model} role={endpoint.role}"
763
+
764
+ def workflow_text(context: McpContext) -> str:
765
+ agents = context.profile_store.list_agents()
766
+ if not agents:
767
+ return "No supervisor workflow agents bound. Use /bind-agent supervisor|main|small <provider> <model>."
768
+ lines = ["Supervisor workflow agents:"]
769
+ for slot in ["supervisor", "main", "small"]:
770
+ endpoint = agents.get(slot)
771
+ if endpoint:
772
+ lines.append(f"- {slot}: {endpoint.provider}:{endpoint.model} role={endpoint.role}")
773
+ else:
774
+ lines.append(f"- {slot}: not bound")
775
+ return "\n".join(lines)
776
+
777
+ def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
778
+ agents = context.profile_store.list_agents()
779
+ supervisor = agents.get("supervisor")
780
+ main_agent = agents.get("main")
781
+ small_agent = agents.get("small")
782
+ memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
783
+ plan = supervisor_plan(supervisor, prompt, memory_context)
784
+ outputs: dict[str, str] = {}
785
+ with ThreadPoolExecutor(max_workers=2) as executor:
786
+ futures = {}
787
+ if main_agent:
788
+ futures[executor.submit(call_agent_endpoint, main_agent, build_main_agent_prompt(prompt, plan, memory_context))] = "main"
789
+ if small_agent:
790
+ futures[executor.submit(call_agent_endpoint, small_agent, build_small_agent_prompt(prompt, plan, memory_context))] = "small"
791
+ for future, slot in list(futures.items()):
792
+ try:
793
+ outputs[slot] = future.result()
794
+ except Exception as error: # noqa: BLE001 - agent boundary
795
+ outputs[slot] = f"error: {model_error_hint(str(error))}"
796
+ if not main_agent:
797
+ outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
798
+ persist_workflow_memory(context, prompt, plan, outputs)
799
+ return "\n".join(
800
+ [
801
+ "Workflow: supervisor -> main + small -> merge",
802
+ "Supervisor plan:",
803
+ plan,
804
+ "",
805
+ "Main agent result:",
806
+ outputs.get("main", "-"),
807
+ "",
808
+ "Small memory agent notes:",
809
+ outputs.get("small", "No small agent bound."),
810
+ ]
811
+ )
812
+
813
+ def persist_workflow_memory(context: McpContext, prompt: str, plan: str, outputs: dict[str, str]) -> None:
814
+ small_notes = outputs.get("small", "").strip()
815
+ main_result = outputs.get("main", "").strip()
816
+ note_parts = [
817
+ f"User request: {prompt}",
818
+ f"Supervisor plan: {trim(plan.strip(), 1200)}" if plan.strip() else "Supervisor plan: -",
819
+ ]
820
+ if small_notes and not small_notes.startswith("error:"):
821
+ note_parts.append(f"Small memory notes: {trim(small_notes, 1200)}")
822
+ else:
823
+ note_parts.append("Small memory notes: unavailable; persisted supervisor plan as fallback RAM note.")
824
+ if main_result and not main_result.startswith("error:"):
825
+ note_parts.append(f"Main result summary: {trim(main_result, 600)}")
826
+ content = "\n".join(note_parts).strip()
827
+ if not content:
828
+ return
829
+ call_tool(
830
+ context,
831
+ "agentram_emit_event",
832
+ {"type": "DECISION", "payload": {"content": content}, "task_id": "codex", "ingest": True},
833
+ )
834
+
835
+ def supervisor_plan(supervisor: AgentModelEndpoint | None, prompt: str, memory_context: str) -> str:
836
+ if not supervisor:
837
+ return "No supervisor bound. Fallback plan: handle user request, run main coding agent, ask small agent to update RAM notes."
838
+ try:
839
+ return call_agent_endpoint(supervisor, build_supervisor_prompt(prompt, memory_context))
840
+ except Exception as error: # noqa: BLE001 - supervisor boundary
841
+ return f"Supervisor error: {error}\nFallback plan: handle user request, run main coding agent, ask small agent to update RAM notes."
842
+
843
+ def call_agent_endpoint(endpoint: AgentModelEndpoint, prompt: str) -> str:
844
+ return MultiModelCodingOrchestrator([endpoint])._call_endpoint(endpoint, prompt)
845
+
846
+ def build_supervisor_prompt(prompt: str, memory_context: str) -> str:
847
+ 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])
848
+
849
+ def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
850
+ 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])
851
+
852
+ def build_small_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
853
+ 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])
854
+
855
+ def bind_router_text(context: McpContext, options: dict[str, object]) -> str:
856
+ api_key_env = str(options.get("api_key_env", ""))
857
+ if api_key_env.startswith(("sk-", "gsk_", "sk_")):
858
+ return "Do not paste raw API key into api_key_env. Set env var first, then use api_key_env=ENV_NAME."
859
+ endpoint = AgentModelEndpoint(
860
+ provider=str(options.get("provider", "openai-compatible")),
861
+ model=str(options.get("model", "")),
862
+ role="router",
863
+ base_url=str(options.get("base_url", "")),
864
+ api_key_env=api_key_env,
865
+ modalities=["text"],
866
+ )
867
+ context.profile_store.set_router(endpoint)
868
+ return f"Bound router: provider={endpoint.provider} model={endpoint.model}"
869
+
870
+ def route_text(context: McpContext, mode: str) -> str:
871
+ normalized = mode.strip().lower() or "status"
872
+ if normalized == "on":
873
+ context.profile_store.set_auto_route(True)
874
+ elif normalized == "off":
875
+ context.profile_store.set_auto_route(False)
876
+ elif normalized != "status":
877
+ return "Usage: /route on|off|status"
878
+ router = context.profile_store.get_router()
879
+ return "\n".join(
880
+ [
881
+ f"Auto route: {'on' if context.profile_store.auto_route_enabled() else 'off'}",
882
+ f"Router model: {router.provider}:{router.model}" if router else "Router model: heuristic fallback",
883
+ ]
884
+ )
885
+
886
+ def route_plain_prompt_text(context: McpContext, prompt: str) -> str:
887
+ decision = classify_prompt_intent(context, prompt)
888
+ intent = str(decision.get("intent", "chat"))
889
+ confidence = float(decision.get("confidence", 0.0))
890
+ prefix = f"Route: {intent} confidence={confidence:.2f}"
891
+ if intent == "coding":
892
+ if context.profile_store.list_endpoints():
893
+ return prefix + "\n" + orchestrate_text(context, prompt, execute=True)
894
+ context_block = retrieve_text(context, prompt, task_id="codex", limit=6)
895
+ return prefix + "\nNo coding models bound. Use /bind-model.\nCoding chat captured. Relevant context:\n" + context_block
896
+ if intent == "planning":
897
+ return prefix + "\n" + drift_text(context, prompt) + "\n\n" + goal_text(context)
898
+ if intent == "memory":
899
+ return prefix + "\n" + note_text(context, prompt)
900
+ context_block = retrieve_text(context, prompt, task_id="codex", limit=6)
901
+ return prefix + "\nCoding chat captured. Relevant context:\n" + context_block
902
+
903
+ def classify_prompt_intent(context: McpContext, prompt: str) -> dict[str, object]:
904
+ router = context.profile_store.get_router()
905
+ if router:
906
+ try:
907
+ response = MultiModelCodingOrchestrator([router])._call_endpoint(router, build_router_prompt(prompt))
908
+ parsed = json.loads(extract_json_object(response))
909
+ if isinstance(parsed, dict) and parsed.get("intent") in {"coding", "planning", "memory", "chat"}:
910
+ return {"intent": parsed.get("intent"), "confidence": float(parsed.get("confidence", 0.0)), "source": "model"}
911
+ except Exception as error: # noqa: BLE001 - router fallback boundary
912
+ return {**heuristic_intent(prompt), "source": "heuristic", "router_error": str(error)}
913
+ return {**heuristic_intent(prompt), "source": "heuristic"}
914
+
915
+ def build_router_prompt(prompt: str) -> str:
916
+ return "\n".join(
917
+ [
918
+ "Classify user prompt for AgentRAM routing.",
919
+ "Return only JSON, no markdown.",
920
+ "Schema: {\"intent\":\"coding|planning|memory|chat\",\"confidence\":0.0,\"reason\":\"short\"}",
921
+ "coding = edit/review/debug/refactor/test/code/file requests.",
922
+ "planning = goal stack, milestone, mission, next task, drift, roadmap.",
923
+ "memory = remember/note/decision/fact to save.",
924
+ "chat = other normal conversation.",
925
+ f"Prompt: {prompt}",
926
+ ]
927
+ )
928
+
929
+ def extract_json_object(text: str) -> str:
930
+ start = text.find("{")
931
+ end = text.rfind("}")
932
+ if start == -1 or end == -1 or end < start:
933
+ return "{}"
934
+ return text[start : end + 1]
935
+
936
+ def heuristic_intent(prompt: str) -> dict[str, object]:
937
+ lower = prompt.lower()
938
+ memory_words = ["ghi nhớ", "remember", "note", "decision", "lưu lại", "save this"]
939
+ planning_words = ["goal", "kế hoạch", "ke hoach", "milestone", "mission", "drift", "next task", "roadmap", "mục tiêu", "muc tieu"]
940
+ coding_words = ["sửa", "sua", "fix", "bug", "code", "test", "refactor", "review", "file", ".py", ".js", "endpoint", "class", "function"]
941
+ if any(word in lower for word in memory_words):
942
+ return {"intent": "memory", "confidence": 0.75}
943
+ if any(word in lower for word in planning_words):
944
+ return {"intent": "planning", "confidence": 0.75}
945
+ if any(word in lower for word in coding_words):
946
+ return {"intent": "coding", "confidence": 0.72}
947
+ return {"intent": "chat", "confidence": 0.55}
948
+
949
+ def orchestrate_text(context: McpContext, task: str, files: list[str] | None = None, execute: bool = False) -> str:
906
950
  if not task.strip():
907
951
  return "Usage: /orchestrate <coding task>"
908
952
  tool = "agentram_run_coding_orchestration" if execute else "agentram_build_coding_orchestration"
@@ -926,16 +970,16 @@ def model_error_hint(message: str) -> str:
926
970
  lower = message.lower()
927
971
  if "base_url is required" in lower:
928
972
  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."
973
+ if "actively refused" in lower or "connection refused" in lower or "winerror 10061" in lower:
974
+ return message + " | Fix: start your OpenAI-compatible server at base_url, or change --base-url."
975
+ if "http 404" in lower or "404: not found" in lower:
976
+ return message + " | Fix: verify base_url includes /v1, provider supports /chat/completions, and model name exists."
977
+ if "http 400" in lower and "not supported" in lower:
978
+ return message + " | Fix: this account/provider rejects that model. Bind a supported model name from your local endpoint."
979
+ if "returned non-json" in lower or "expecting value" in lower:
980
+ return message + " | Fix: endpoint returned empty/HTML/non-JSON. Check base_url, auth key, proxy, and server logs."
981
+ if "missing api key env" in lower:
982
+ return message + " | Fix: set environment variable, then bind using --api-key-env ENV_NAME, not raw key."
939
983
  return message
940
984
 
941
985
  def claude_template_root() -> Path:
@@ -1004,7 +1048,7 @@ BUTTONS = {
1004
1048
  }
1005
1049
 
1006
1050
 
1007
- def draw_screen(context: McpContext, messages: list[str], activity: str | None = None) -> None:
1051
+ def draw_screen(context: McpContext, messages: list[str], activity: str | None = None) -> None:
1008
1052
  clear_terminal()
1009
1053
  width = min(max(shutil.get_terminal_size((110, 34)).columns, 86), 132)
1010
1054
  left_width = 30
@@ -1013,17 +1057,17 @@ def draw_screen(context: McpContext, messages: list[str], activity: str | None =
1013
1057
  print(box_text("◆ AgentRAM Code | RAM + Claude subagents + slash commands", width))
1014
1058
  print("+" + "-" * (width - 2) + "+")
1015
1059
 
1016
- sidebar = sidebar_lines(context)
1017
- display_messages = [*messages, activity] if activity else messages
1018
- chat = wrap_messages(display_messages, right_width - 4)[-22:]
1060
+ sidebar = sidebar_lines(context)
1061
+ display_messages = [*messages, activity] if activity else messages
1062
+ chat = wrap_messages(display_messages, right_width - 4)[-22:]
1019
1063
  rows = max(len(sidebar), len(chat), 22)
1020
1064
  for index in range(rows):
1021
1065
  left = sidebar[index] if index < len(sidebar) else ""
1022
1066
  right = chat[index] if index < len(chat) else ""
1023
1067
  print("| " + left[: left_width - 2].ljust(left_width - 2) + "| " + right[: right_width - 2].ljust(right_width - 2) + "|")
1024
1068
  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))
1069
+ footer = "Thinking/acting..." if activity else "Input: type / for commands. Use mouse wheel or terminal scrollbar for previous output."
1070
+ print(box_text(footer, width))
1027
1071
  print("+" + "-" * (width - 2) + "+")
1028
1072
 
1029
1073
 
@@ -1062,8 +1106,8 @@ def sidebar_lines(context: McpContext) -> list[str]:
1062
1106
  " /ask <prompt>",
1063
1107
  ]
1064
1108
 
1065
-
1066
-
1109
+
1110
+
1067
1111
  def trim(text: str, limit: int) -> str:
1068
1112
  return text if len(text) <= limit else text[: limit - 1] + "…"
1069
1113
 
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  const { spawnSync } = require("node:child_process");
3
2
 
4
3
  if (process.env.AGENTRAM_SKIP_PY_DEPS === "1") {
package/package.json CHANGED
@@ -1,52 +1,52 @@
1
- {
2
- "name": "agentram",
3
- "version": "0.1.12",
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.14",
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
+ }
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.12"
7
+ version = "0.1.14"
8
8
  description = "Async, model-agnostic RAM layer for agentic coding tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"