agentram 0.1.0 → 0.1.2
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/.claude/agents/agentram-docs.md +18 -0
- package/.claude/agents/agentram-memory.md +20 -0
- package/.claude/agents/agentram-planner.md +18 -0
- package/.claude/agents/agentram-reviewer.md +18 -0
- package/.claude/commands/ram-drift.md +11 -0
- package/.claude/commands/ram-note.md +17 -0
- package/.claude/commands/ram-orchestrate.md +15 -0
- package/.claude/commands/ram-status.md +10 -0
- package/.claude/commands/ram.md +14 -0
- package/README.md +117 -0
- package/agentram/__init__.py +4 -0
- package/agentram/cli.py +547 -240
- package/agentram/mcp_server.py +107 -1
- package/agentram/orchestration.py +145 -0
- package/agentram/storage.py +40 -0
- package/package.json +3 -1
- package/pyproject.toml +1 -1
package/agentram/cli.py
CHANGED
|
@@ -1,242 +1,549 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import os
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
from .
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
import textwrap
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .config import default_ram_root
|
|
12
|
+
from .mcp_server import McpContext, call_tool
|
|
13
|
+
|
|
14
|
+
SUBAGENTS: dict[str, str] = {
|
|
15
|
+
"memory": "Summarize important facts, decisions, files, risks, and next tasks into AgentRAM events. Do not edit code.",
|
|
16
|
+
"planner": "Break the coding request into small ordered steps. Check AgentRAM goal drift before scope changes.",
|
|
17
|
+
"reviewer": "Review code changes for correctness, missing tests, regressions, and risky assumptions.",
|
|
18
|
+
"documenter": "Update README or usage notes from current implementation. Keep docs concise and runnable.",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
SLASH_HELP = """Slash commands:
|
|
22
|
+
/help Show commands
|
|
23
|
+
/init Create .agentram files
|
|
24
|
+
/status Show RAM status
|
|
25
|
+
/retrieve <query> Print relevant RAM context
|
|
26
|
+
/goal Show Goal Stack
|
|
27
|
+
/set-goal key=value ... Update mission/current_goal/current_task/etc
|
|
28
|
+
/drift <task> Check goal drift
|
|
29
|
+
/events Show latest events
|
|
30
|
+
/replay Replay events into memory
|
|
31
|
+
/agents List subagent presets
|
|
32
|
+
/agent <name> <task> Build Claude Code subagent prompt
|
|
33
|
+
/models List bound coding models
|
|
34
|
+
/bind-model provider model Bind model endpoint
|
|
35
|
+
/orchestrate <task> Build multi-model agent plan
|
|
36
|
+
/claude-install [path] Install .claude commands and agents
|
|
37
|
+
/note <text> Save decision note
|
|
38
|
+
/clear Clear chat
|
|
39
|
+
/exit Quit
|
|
40
|
+
Plain text Capture coding chat message and retrieve context
|
|
41
|
+
""".strip()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
45
|
+
parser = argparse.ArgumentParser(description="AgentRAM terminal UI and CLI.")
|
|
46
|
+
parser.add_argument("--ram-root", default=None, help="AgentRAM storage root. Defaults to .agentram in the current project directory.")
|
|
47
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
48
|
+
|
|
49
|
+
subparsers.add_parser("tui", help="Open terminal chat UI.")
|
|
50
|
+
subparsers.add_parser("init", help="Create AgentRAM files.")
|
|
51
|
+
subparsers.add_parser("status", help="Show RAM file and item counts.")
|
|
52
|
+
subparsers.add_parser("goal", help="Show goal stack.")
|
|
53
|
+
subparsers.add_parser("agents", help="List subagent presets.")
|
|
54
|
+
subparsers.add_parser("models", help="List bound coding model endpoints.")
|
|
55
|
+
|
|
56
|
+
bind_model = subparsers.add_parser("bind-model", help="Bind a coding model endpoint.")
|
|
57
|
+
bind_model.add_argument("provider", help="Provider name, e.g. claude, openai-compatible, groq.")
|
|
58
|
+
bind_model.add_argument("model", help="Model name.")
|
|
59
|
+
bind_model.add_argument("--role", default="coder", help="Agent role.")
|
|
60
|
+
bind_model.add_argument("--base-url", default="", help="Provider base URL.")
|
|
61
|
+
bind_model.add_argument("--api-key-env", default="", help="Environment variable containing API key.")
|
|
62
|
+
bind_model.add_argument("--modalities", default="text", help="Comma-separated modalities.")
|
|
63
|
+
|
|
64
|
+
orchestrate = subparsers.add_parser("orchestrate", help="Build or run multi-model coding orchestration.")
|
|
65
|
+
orchestrate.add_argument("task", nargs="?", default="", help="Coding task.")
|
|
66
|
+
orchestrate.add_argument("--file", action="append", default=[], help="File path to include.")
|
|
67
|
+
orchestrate.add_argument("--execute", action="store_true", help="Call model endpoints concurrently.")
|
|
68
|
+
|
|
69
|
+
retrieve = subparsers.add_parser("retrieve", help="Retrieve memory context.")
|
|
70
|
+
retrieve.add_argument("query", nargs="?", default="", help="Search query.")
|
|
71
|
+
retrieve.add_argument("--task-id", default="codex", help="Task id.")
|
|
72
|
+
retrieve.add_argument("--limit", type=int, default=8, help="Max items.")
|
|
73
|
+
|
|
74
|
+
drift = subparsers.add_parser("drift", help="Check goal drift for a task.")
|
|
75
|
+
drift.add_argument("task", nargs="?", default="", help="Task to check.")
|
|
76
|
+
|
|
77
|
+
replay = subparsers.add_parser("replay", help="Replay events into memory.")
|
|
78
|
+
replay.add_argument("--task-id", default=None, help="Optional task id filter.")
|
|
79
|
+
|
|
80
|
+
claude_install = subparsers.add_parser("claude-install", help="Install Claude Code slash commands and subagents into a project.")
|
|
81
|
+
claude_install.add_argument("--target", default=".", help="Target project root.")
|
|
82
|
+
claude_install.add_argument("--force", action="store_true", help="Overwrite existing templates.")
|
|
83
|
+
|
|
84
|
+
agent = subparsers.add_parser("agent", help="Build subagent prompt.")
|
|
85
|
+
agent.add_argument("name", choices=sorted(SUBAGENTS), help="Subagent preset.")
|
|
86
|
+
agent.add_argument("task", nargs="*", help="Task for subagent.")
|
|
87
|
+
|
|
88
|
+
return parser
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def context_from_args(args: argparse.Namespace) -> McpContext:
|
|
92
|
+
ram_root = Path(args.ram_root) if args.ram_root else default_ram_root()
|
|
93
|
+
return McpContext(ram_root=ram_root)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def payload(result: dict[str, Any]) -> dict[str, Any]:
|
|
97
|
+
return dict(result.get("structuredContent", {}))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run_command(args: argparse.Namespace) -> int:
|
|
101
|
+
context = context_from_args(args)
|
|
102
|
+
command = args.command or "tui"
|
|
103
|
+
|
|
104
|
+
if command == "tui":
|
|
105
|
+
run_tui(context)
|
|
106
|
+
return 0
|
|
107
|
+
if command == "init":
|
|
108
|
+
print(init_text(context))
|
|
109
|
+
return 0
|
|
110
|
+
if command == "status":
|
|
111
|
+
print(status_text(context))
|
|
112
|
+
return 0
|
|
113
|
+
if command == "retrieve":
|
|
114
|
+
query = args.query or input("Query > ").strip()
|
|
115
|
+
print(retrieve_text(context, query, task_id=args.task_id, limit=args.limit))
|
|
116
|
+
return 0
|
|
117
|
+
if command == "goal":
|
|
118
|
+
print(goal_text(context))
|
|
119
|
+
return 0
|
|
120
|
+
if command == "drift":
|
|
121
|
+
task = args.task or input("Task > ").strip()
|
|
122
|
+
print(drift_text(context, task))
|
|
123
|
+
return 0
|
|
124
|
+
if command == "replay":
|
|
125
|
+
arguments = {"task_id": args.task_id} if args.task_id else {}
|
|
126
|
+
print(replay_text(context, arguments))
|
|
127
|
+
return 0
|
|
128
|
+
if command == "agents":
|
|
129
|
+
print(agents_text())
|
|
130
|
+
return 0
|
|
131
|
+
if command == "models":
|
|
132
|
+
print(models_text(context))
|
|
133
|
+
return 0
|
|
134
|
+
if command == "bind-model":
|
|
135
|
+
print(bind_model_text(context, vars(args)))
|
|
136
|
+
return 0
|
|
137
|
+
if command == "orchestrate":
|
|
138
|
+
task = args.task or input("Task > ").strip()
|
|
139
|
+
print(orchestrate_text(context, task, files=args.file, execute=args.execute))
|
|
140
|
+
return 0
|
|
141
|
+
if command == "claude-install":
|
|
142
|
+
print(install_claude_templates_text(Path(args.target), force=args.force))
|
|
143
|
+
return 0
|
|
144
|
+
if command == "agent":
|
|
145
|
+
print(agent_prompt_text(context, args.name, " ".join(args.task)))
|
|
146
|
+
return 0
|
|
147
|
+
|
|
102
148
|
raise SystemExit(f"unknown command: {command}")
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def run_tui(context: McpContext) -> None:
|
|
106
|
-
context.init_storage()
|
|
107
|
-
messages = ["AgentRAM
|
|
108
|
-
while True:
|
|
109
|
-
draw_screen(context, messages)
|
|
110
|
-
|
|
111
|
-
if
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def run_tui(context: McpContext) -> None:
|
|
152
|
+
context.init_storage()
|
|
153
|
+
messages = ["system > AgentRAM coding-agent CLI ready", f"system > RAM root: {context.ram_root}", "system > Type /help, /agent reviewer <task>, or plain coding request."]
|
|
154
|
+
while True:
|
|
155
|
+
draw_screen(context, messages)
|
|
156
|
+
user_input = input("You > ").strip()
|
|
157
|
+
if user_input in {"0", "q", "quit", "exit", "/exit", "/quit"}:
|
|
158
|
+
return
|
|
159
|
+
if user_input in BUTTONS:
|
|
160
|
+
user_input = BUTTONS[user_input]
|
|
161
|
+
if user_input:
|
|
162
|
+
messages.append(f"user > {user_input}")
|
|
163
|
+
response = handle_chat_input(context, user_input, messages)
|
|
164
|
+
if response == "__clear__":
|
|
165
|
+
messages.clear()
|
|
166
|
+
messages.append("system > Chat cleared.")
|
|
167
|
+
elif response:
|
|
168
|
+
messages.append("agentram > " + response)
|
|
169
|
+
del messages[:-10]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def handle_chat_input(context: McpContext, text: str, messages: list[str] | None = None) -> str:
|
|
173
|
+
if not text:
|
|
174
|
+
return SLASH_HELP
|
|
175
|
+
if text.startswith("/"):
|
|
176
|
+
return handle_slash_command(context, text, messages)
|
|
177
|
+
call_tool(
|
|
178
|
+
context,
|
|
179
|
+
"agentram_emit_event",
|
|
180
|
+
{"type": "USER_MESSAGE", "payload": {"content": text}, "task_id": "codex", "ingest": False},
|
|
181
|
+
)
|
|
182
|
+
context_block = retrieve_text(context, text, task_id="codex", limit=6)
|
|
183
|
+
return "Coding chat captured. Relevant context:\n" + context_block
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def handle_slash_command(context: McpContext, text: str, messages: list[str] | None = None) -> str:
|
|
187
|
+
parts = shlex.split(text, posix=os.name != 'nt')
|
|
188
|
+
command = parts[0].lower()
|
|
189
|
+
rest = parts[1:]
|
|
190
|
+
raw_rest = text[len(parts[0]) :].strip()
|
|
191
|
+
|
|
192
|
+
if command == "/help":
|
|
193
|
+
return SLASH_HELP
|
|
194
|
+
if command == "/init":
|
|
195
|
+
return init_text(context)
|
|
196
|
+
if command == "/status":
|
|
197
|
+
return status_text(context)
|
|
198
|
+
if command == "/retrieve":
|
|
199
|
+
return retrieve_text(context, raw_rest, task_id="codex", limit=8)
|
|
200
|
+
if command == "/goal":
|
|
201
|
+
return goal_text(context)
|
|
202
|
+
if command == "/set-goal":
|
|
203
|
+
return set_goal_text(context, rest)
|
|
204
|
+
if command == "/drift":
|
|
205
|
+
return drift_text(context, raw_rest)
|
|
206
|
+
if command == "/events":
|
|
207
|
+
return events_text(context)
|
|
208
|
+
if command == "/replay":
|
|
209
|
+
return replay_text(context, {})
|
|
210
|
+
if command == "/agents":
|
|
211
|
+
return agents_text()
|
|
212
|
+
if command == "/models":
|
|
213
|
+
return models_text(context)
|
|
214
|
+
if command == "/bind-model":
|
|
215
|
+
if len(rest) < 2:
|
|
216
|
+
return "Usage: /bind-model <provider> <model> [role=coder] [base_url=...] [api_key_env=...] [modalities=text,image]"
|
|
217
|
+
options = {"provider": rest[0], "model": rest[1]}
|
|
218
|
+
for item in rest[2:]:
|
|
219
|
+
if "=" in item:
|
|
220
|
+
key, value = item.split("=", 1)
|
|
221
|
+
options[key.replace("-", "_")] = value
|
|
222
|
+
return bind_model_text(context, options)
|
|
223
|
+
if command == "/orchestrate":
|
|
224
|
+
return orchestrate_text(context, raw_rest, execute=False)
|
|
225
|
+
if command == "/agent":
|
|
226
|
+
if not rest:
|
|
227
|
+
return "Usage: /agent <memory|planner|reviewer|documenter> <task>"
|
|
228
|
+
return agent_prompt_text(context, rest[0], " ".join(rest[1:]))
|
|
229
|
+
if command == "/claude-install":
|
|
230
|
+
force = "--force" in raw_rest
|
|
231
|
+
target_text = raw_rest.replace("--force", "").strip().strip('"')
|
|
232
|
+
target = Path(target_text) if target_text else Path.cwd()
|
|
233
|
+
return install_claude_templates_text(target, force=force)
|
|
234
|
+
if command == "/note":
|
|
235
|
+
return note_text(context, raw_rest)
|
|
236
|
+
if command == "/clear":
|
|
237
|
+
return "__clear__"
|
|
238
|
+
return f"Unknown slash command: {command}\n\n{SLASH_HELP}"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def init_text(context: McpContext) -> str:
|
|
242
|
+
data = payload(call_tool(context, "agentram_init", {}))
|
|
243
|
+
created = data.get("created", [])
|
|
244
|
+
return f"AgentRAM root: {data['ram_root']}\nCreated: {', '.join(created) if created else 'none'}"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def status_text(context: McpContext) -> str:
|
|
248
|
+
context.init_storage()
|
|
249
|
+
events = context.event_log.read_all()
|
|
250
|
+
items = context.memory_store.list_items()
|
|
251
|
+
goal_state = context.goal_store.load()
|
|
252
|
+
return "\n".join(
|
|
253
|
+
[
|
|
254
|
+
f"RAM root: {context.ram_root}",
|
|
255
|
+
f"Events: {len(events)}",
|
|
256
|
+
f"Memory items: {len(items)}",
|
|
257
|
+
f"Mission: {goal_state.mission or '-'}",
|
|
258
|
+
f"Current goal: {goal_state.current_goal or '-'}",
|
|
259
|
+
f"Current task: {goal_state.current_task or '-'}",
|
|
260
|
+
]
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def retrieve_text(context: McpContext, query: str, task_id: str = "codex", limit: int = 8) -> str:
|
|
265
|
+
if not query.strip():
|
|
266
|
+
return "Usage: /retrieve <query>"
|
|
267
|
+
data = payload(call_tool(context, "agentram_retrieve", {"query": query, "task_id": task_id, "limit": limit}))
|
|
268
|
+
return data.get("context") or "No matching memory."
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def goal_text(context: McpContext) -> str:
|
|
272
|
+
data = payload(call_tool(context, "agentram_read_goal_state", {}))
|
|
273
|
+
return data.get("context") or "No goal stack yet."
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def set_goal_text(context: McpContext, assignments: list[str]) -> str:
|
|
277
|
+
if not assignments:
|
|
278
|
+
return "Usage: /set-goal current_goal='Ship CLI' current_task='Add slash commands'"
|
|
279
|
+
patch: dict[str, object] = {}
|
|
280
|
+
for item in assignments:
|
|
281
|
+
if "=" not in item:
|
|
282
|
+
return f"Invalid assignment: {item}"
|
|
283
|
+
key, value = item.split("=", 1)
|
|
284
|
+
if key == "next_tasks":
|
|
285
|
+
patch[key] = [part.strip() for part in value.split(",") if part.strip()]
|
|
286
|
+
else:
|
|
287
|
+
patch[key] = value
|
|
288
|
+
data = payload(call_tool(context, "agentram_update_goal_state", patch))
|
|
289
|
+
return data.get("context") or "Goal updated."
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def drift_text(context: McpContext, task: str) -> str:
|
|
293
|
+
if not task.strip():
|
|
294
|
+
return "Usage: /drift <task>"
|
|
295
|
+
data = payload(call_tool(context, "agentram_check_goal_drift", {"task": task}))
|
|
296
|
+
lines = [f"Status: {data.get('status')}", f"Aligned: {data.get('aligned')}", f"Reason: {data.get('reason')}"]
|
|
297
|
+
if data.get("warning"):
|
|
298
|
+
lines.append(f"Warning: {data['warning']}")
|
|
299
|
+
return "\n".join(lines)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def events_text(context: McpContext) -> str:
|
|
303
|
+
events = context.event_log.read_all()
|
|
304
|
+
latest = events[-8:]
|
|
305
|
+
lines = [f"Events: {len(events)}"]
|
|
306
|
+
lines.extend(f"- {event.type.value} {event.payload}" for event in latest)
|
|
307
|
+
return "\n".join(lines)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def replay_text(context: McpContext, arguments: dict[str, object]) -> str:
|
|
311
|
+
data = payload(call_tool(context, "agentram_replay_events", arguments))
|
|
312
|
+
return f"Events: {data.get('event_count', 0)}\nPatches: {data.get('patch_count', 0)}\nMemory items: {data.get('memory_count', 0)}"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def agents_text() -> str:
|
|
316
|
+
lines = ["Subagent presets:"]
|
|
317
|
+
lines.extend(f"- {name}: {description}" for name, description in SUBAGENTS.items())
|
|
318
|
+
return "\n".join(lines)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def agent_prompt_text(context: McpContext, name: str, task: str) -> str:
|
|
322
|
+
if name not in SUBAGENTS:
|
|
323
|
+
return f"Unknown subagent: {name}\n\n{agents_text()}"
|
|
324
|
+
memory = retrieve_text(context, task or name, task_id="codex", limit=6) if task else goal_text(context)
|
|
325
|
+
return "\n".join(
|
|
326
|
+
[
|
|
327
|
+
f"Subagent: {name}",
|
|
328
|
+
f"Role: {SUBAGENTS[name]}",
|
|
329
|
+
f"Task: {task or '-'}",
|
|
330
|
+
"Context:",
|
|
331
|
+
memory,
|
|
332
|
+
"Output contract:",
|
|
333
|
+
"- Return concise findings.",
|
|
334
|
+
"- Mention files touched or inspected.",
|
|
335
|
+
"- Do not invent facts outside repo/RAM.",
|
|
336
|
+
]
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def models_text(context: McpContext) -> str:
|
|
343
|
+
data = payload(call_tool(context, "agentram_read_coding_models", {}))
|
|
344
|
+
endpoints = data.get("endpoints", [])
|
|
345
|
+
if not endpoints:
|
|
346
|
+
return "No coding models bound. Use /bind-model claude claude-3-5-sonnet-latest role=reviewer api_key_env=ANTHROPIC_API_KEY"
|
|
347
|
+
lines = ["Bound coding models:"]
|
|
348
|
+
for endpoint in endpoints:
|
|
349
|
+
lines.append(f"- {endpoint.get('id')} provider={endpoint.get('provider')} model={endpoint.get('model')} role={endpoint.get('role')} modalities={','.join(endpoint.get('modalities', []))}")
|
|
350
|
+
return "\n".join(lines)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def bind_model_text(context: McpContext, options: dict[str, object]) -> str:
|
|
354
|
+
modalities_value = str(options.get("modalities", "text"))
|
|
355
|
+
arguments = {
|
|
356
|
+
"id": str(options.get("id", "")) or None,
|
|
357
|
+
"provider": str(options.get("provider", "openai-compatible")),
|
|
358
|
+
"model": str(options.get("model", "")),
|
|
359
|
+
"role": str(options.get("role", "coder")),
|
|
360
|
+
"base_url": str(options.get("base_url", "")),
|
|
361
|
+
"api_key_env": str(options.get("api_key_env", "")),
|
|
362
|
+
"modalities": [item.strip() for item in modalities_value.split(",") if item.strip()],
|
|
363
|
+
"enabled": bool(options.get("enabled", True)),
|
|
364
|
+
}
|
|
365
|
+
arguments = {key: value for key, value in arguments.items() if value is not None}
|
|
366
|
+
data = payload(call_tool(context, "agentram_bind_coding_model", arguments))
|
|
367
|
+
endpoint = data.get("endpoint", {})
|
|
368
|
+
return f"Bound model: {endpoint.get('id')} provider={endpoint.get('provider')} model={endpoint.get('model')} role={endpoint.get('role')}"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def orchestrate_text(context: McpContext, task: str, files: list[str] | None = None, execute: bool = False) -> str:
|
|
372
|
+
if not task.strip():
|
|
373
|
+
return "Usage: /orchestrate <coding task>"
|
|
374
|
+
tool = "agentram_run_coding_orchestration" if execute else "agentram_build_coding_orchestration"
|
|
375
|
+
data = payload(call_tool(context, tool, {"task": task, "files": files or [], "execute": execute}))
|
|
376
|
+
if not execute:
|
|
377
|
+
lines = [f"Orchestration plan: agents={data.get('agent_count', 0)}"]
|
|
378
|
+
for agent in data.get("agents", []):
|
|
379
|
+
endpoint = agent.get("endpoint", {})
|
|
380
|
+
lines.append(f"- {endpoint.get('id')} {endpoint.get('provider')}:{endpoint.get('model')} role={endpoint.get('role')}")
|
|
381
|
+
return "\n".join(lines)
|
|
382
|
+
lines = [f"Orchestration run: agents={data.get('agent_count', 0)}"]
|
|
383
|
+
for result in data.get("results", []):
|
|
384
|
+
endpoint = result.get("endpoint", {})
|
|
385
|
+
status = "ok" if result.get("ok") else "error"
|
|
386
|
+
output = result.get("output") or result.get("error", "")
|
|
387
|
+
lines.append(f"- {endpoint.get('id')} {status}: {str(output)[:300]}")
|
|
388
|
+
return "\n".join(lines)
|
|
389
|
+
|
|
390
|
+
def claude_template_root() -> Path:
|
|
391
|
+
return Path(__file__).resolve().parent.parent / ".claude"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def install_claude_templates_text(target: Path, force: bool = False) -> str:
|
|
395
|
+
source_root = claude_template_root()
|
|
396
|
+
if not source_root.exists():
|
|
397
|
+
return f"Claude templates not found: {source_root}"
|
|
398
|
+
target_root = target.resolve() / ".claude"
|
|
399
|
+
copied: list[str] = []
|
|
400
|
+
skipped: list[str] = []
|
|
401
|
+
for source in source_root.rglob("*.md"):
|
|
402
|
+
relative = source.relative_to(source_root)
|
|
403
|
+
destination = target_root / relative
|
|
404
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
405
|
+
if destination.exists() and not force:
|
|
406
|
+
skipped.append(str(destination))
|
|
407
|
+
continue
|
|
408
|
+
destination.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
|
|
409
|
+
copied.append(str(destination))
|
|
410
|
+
lines = [f"Claude templates target: {target_root}", f"Copied: {len(copied)}", f"Skipped: {len(skipped)}"]
|
|
411
|
+
if copied:
|
|
412
|
+
lines.append("Installed:")
|
|
413
|
+
lines.extend(f"- {item}" for item in copied)
|
|
414
|
+
if skipped:
|
|
415
|
+
lines.append("Skipped existing files. Use --force to overwrite.")
|
|
416
|
+
return "\n".join(lines)
|
|
417
|
+
|
|
418
|
+
def note_text(context: McpContext, content: str) -> str:
|
|
419
|
+
if not content.strip():
|
|
420
|
+
return "Usage: /note <decision or fact>"
|
|
421
|
+
data = payload(call_tool(context, "agentram_emit_event", {"type": "DECISION", "payload": {"content": content}, "task_id": "codex", "ingest": True}))
|
|
422
|
+
ingest = data.get("ingest") or {}
|
|
423
|
+
return f"Note saved. Patches: {ingest.get('patch_count', 0)}"
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def update_goal_from_prompt(context: McpContext, messages: list[str]) -> None:
|
|
427
|
+
patch: dict[str, object] = {}
|
|
428
|
+
fields = ["mission", "project_goal", "current_milestone", "current_goal", "current_task"]
|
|
429
|
+
for field in fields:
|
|
430
|
+
value = input(f"{field} (blank skip) > ").strip()
|
|
431
|
+
if value:
|
|
432
|
+
patch[field] = value
|
|
433
|
+
next_tasks = input("next_tasks comma list (blank skip) > ").strip()
|
|
434
|
+
if next_tasks:
|
|
435
|
+
patch["next_tasks"] = [item.strip() for item in next_tasks.split(",") if item.strip()]
|
|
436
|
+
if not patch:
|
|
437
|
+
messages.append("Goal update skipped.")
|
|
438
|
+
return
|
|
439
|
+
data = payload(call_tool(context, "agentram_update_goal_state", patch))
|
|
440
|
+
messages.append(data.get("context") or "Goal updated.")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
BUTTONS = {
|
|
444
|
+
"1": "/init",
|
|
445
|
+
"2": "/status",
|
|
446
|
+
"3": "/retrieve",
|
|
447
|
+
"4": "/goal",
|
|
448
|
+
"5": "/set-goal",
|
|
449
|
+
"6": "/drift",
|
|
450
|
+
"7": "/events",
|
|
451
|
+
"8": "/replay",
|
|
452
|
+
"9": "/help",
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def draw_screen(context: McpContext, messages: list[str]) -> None:
|
|
457
|
+
clear_terminal()
|
|
458
|
+
width = min(max(shutil.get_terminal_size((110, 34)).columns, 86), 132)
|
|
459
|
+
left_width = 30
|
|
460
|
+
right_width = width - left_width - 3
|
|
461
|
+
print("+" + "-" * (width - 2) + "+")
|
|
462
|
+
print(box_text("AgentRAM Code Agent CLI | RAM + Claude subagents + slash commands", width))
|
|
463
|
+
print("+" + "-" * (width - 2) + "+")
|
|
464
|
+
|
|
465
|
+
sidebar = sidebar_lines(context)
|
|
466
|
+
chat = wrap_messages(messages, right_width - 4)[-22:]
|
|
467
|
+
rows = max(len(sidebar), len(chat), 22)
|
|
468
|
+
for index in range(rows):
|
|
469
|
+
left = sidebar[index] if index < len(sidebar) else ""
|
|
470
|
+
right = chat[index] if index < len(chat) else ""
|
|
471
|
+
print("| " + left[: left_width - 2].ljust(left_width - 2) + "| " + right[: right_width - 2].ljust(right_width - 2) + "|")
|
|
472
|
+
print("+" + "-" * (left_width - 1) + "+" + "-" * (right_width) + "+")
|
|
473
|
+
print(box_text("Input: plain coding request, /help, /agent reviewer <task>, /claude-install .", width))
|
|
474
|
+
print("+" + "-" * (width - 2) + "+")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def sidebar_lines(context: McpContext) -> list[str]:
|
|
478
|
+
context.init_storage()
|
|
479
|
+
events = context.event_log.read_all()
|
|
480
|
+
items = context.memory_store.list_items()
|
|
481
|
+
goal = context.goal_store.load()
|
|
482
|
+
return [
|
|
483
|
+
"MODE",
|
|
484
|
+
" coding-agent TUI",
|
|
485
|
+
"",
|
|
486
|
+
"RAM",
|
|
487
|
+
f" events: {len(events)}",
|
|
488
|
+
f" notes: {len(items)}",
|
|
489
|
+
"",
|
|
490
|
+
"GOAL",
|
|
491
|
+
f" {trim(goal.current_goal or '-', 24)}",
|
|
492
|
+
"",
|
|
493
|
+
"TASK",
|
|
494
|
+
f" {trim(goal.current_task or '-', 24)}",
|
|
495
|
+
"",
|
|
496
|
+
"BUTTONS",
|
|
497
|
+
" 1 init 2 status",
|
|
498
|
+
" 3 retrieve 4 goal",
|
|
499
|
+
" 5 set-goal 6 drift",
|
|
500
|
+
" 7 events 8 replay",
|
|
501
|
+
" 9 help 0 exit",
|
|
502
|
+
"",
|
|
503
|
+
"CLAUDE",
|
|
504
|
+
" /claude-install .",
|
|
505
|
+
" /models",
|
|
506
|
+
" /bind-model claude <model>",
|
|
507
|
+
" /orchestrate <task>",
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def trim(text: str, limit: int) -> str:
|
|
512
|
+
return text if len(text) <= limit else text[: limit - 1] + "…"
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def menu_lines() -> list[str]:
|
|
516
|
+
return [
|
|
517
|
+
"1=/init 2=/status 3=/retrieve 4=/goal 5=/set-goal 6=/drift 7=/events 8=/replay 9=/help",
|
|
518
|
+
"Plain text captures coding request. Claude: /claude-install ., /agents, /agent reviewer <task>",
|
|
519
|
+
]
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def wrap_messages(messages: list[str], width: int) -> list[str]:
|
|
523
|
+
lines: list[str] = []
|
|
524
|
+
for message in messages:
|
|
525
|
+
for raw_line in str(message).splitlines() or [""]:
|
|
526
|
+
wrapped = textwrap.wrap(raw_line, width=width, replace_whitespace=False) or [""]
|
|
527
|
+
lines.extend(wrapped)
|
|
528
|
+
return lines
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def box_line(left: str, fill: str, right: str, width: int) -> str:
|
|
532
|
+
return left + fill * (width - 2) + right
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def box_text(text: str, width: int) -> str:
|
|
536
|
+
return "| " + text[: width - 4].ljust(width - 4) + " |"
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def clear_terminal() -> None:
|
|
540
|
+
os.system("cls" if os.name == "nt" else "clear")
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def main(argv: list[str] | None = None) -> int:
|
|
544
|
+
args = build_parser().parse_args(argv)
|
|
545
|
+
return run_command(args)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
if __name__ == "__main__":
|
|
242
549
|
raise SystemExit(main())
|