agentram 0.1.1 → 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/commands/ram-orchestrate.md +15 -0
- package/README.md +51 -0
- package/agentram/__init__.py +4 -0
- package/agentram/cli.py +92 -2
- package/agentram/mcp_server.py +107 -1
- package/agentram/orchestration.py +145 -0
- package/agentram/storage.py +40 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Build or run AgentRAM multi-model coding orchestration.
|
|
3
|
+
argument-hint: [task]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Use AgentRAM multi-model orchestration for this task.
|
|
7
|
+
|
|
8
|
+
1. Call `agentram_read_coding_models`.
|
|
9
|
+
2. If no endpoints exist, ask user to bind a model with `agentram_bind_coding_model`.
|
|
10
|
+
3. Call `agentram_build_coding_orchestration` with:
|
|
11
|
+
- `task`: `$ARGUMENTS`
|
|
12
|
+
- `files`: relevant files if known
|
|
13
|
+
- `modalities`: `["text"]` unless images/audio are included
|
|
14
|
+
4. If user explicitly requests execution, call `agentram_run_coding_orchestration` with `execute: true`.
|
|
15
|
+
5. Summarize per-model outputs and disagreements.
|
package/README.md
CHANGED
|
@@ -352,6 +352,53 @@ Minimum usage pattern:
|
|
|
352
352
|
3. Call `agentram_check_goal_drift` before switching direction.
|
|
353
353
|
4. Call `agentram_retrieve` again near context pressure or resume.
|
|
354
354
|
|
|
355
|
+
|
|
356
|
+
## Multi-Model Coding Orchestration
|
|
357
|
+
|
|
358
|
+
AgentRAM can bind multiple coding model endpoints and let Claude Code or MCP clients orchestrate them in parallel. This keeps the `free-code` style command registry, but adds project RAM, Goal Stack, and multi-agent model routing.
|
|
359
|
+
|
|
360
|
+
Bind models:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
agentram bind-model claude claude-3-5-sonnet-latest --role reviewer --api-key-env ANTHROPIC_API_KEY
|
|
364
|
+
agentram bind-model openai-compatible qwen2.5-coder --role coder --base-url http://localhost:8000/v1 --api-key-env OPENAI_API_KEY
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Build a dry-run orchestration plan:
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
agentram orchestrate "review CLI orchestration" --file agentram/cli.py
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Run endpoints concurrently when keys and base URLs are configured:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
agentram orchestrate "review CLI orchestration" --file agentram/cli.py --execute
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Claude/MCP tools:
|
|
380
|
+
|
|
381
|
+
```text
|
|
382
|
+
agentram_bind_coding_model
|
|
383
|
+
agentram_read_coding_models
|
|
384
|
+
agentram_build_coding_orchestration
|
|
385
|
+
agentram_run_coding_orchestration
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
TUI slash commands:
|
|
389
|
+
|
|
390
|
+
```text
|
|
391
|
+
/bind-model claude claude-3-5-sonnet-latest role=reviewer api_key_env=ANTHROPIC_API_KEY
|
|
392
|
+
/models
|
|
393
|
+
/orchestrate review CLI orchestration
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Storage:
|
|
397
|
+
|
|
398
|
+
```text
|
|
399
|
+
.agentram/agent_profile.json
|
|
400
|
+
```
|
|
401
|
+
|
|
355
402
|
## MCP Tools
|
|
356
403
|
|
|
357
404
|
| Tool | Purpose |
|
|
@@ -364,6 +411,10 @@ Minimum usage pattern:
|
|
|
364
411
|
| `agentram_read_goal_state` | Read mission, goals, current task, histories. |
|
|
365
412
|
| `agentram_update_goal_state` | Update Goal Stack fields. |
|
|
366
413
|
| `agentram_check_goal_drift` | Warn only when task appears off-goal. |
|
|
414
|
+
| `agentram_bind_coding_model` | Bind provider/model/role/modalities endpoint. |
|
|
415
|
+
| `agentram_read_coding_models` | Read bound coding model endpoints. |
|
|
416
|
+
| `agentram_build_coding_orchestration` | Build multi-agent prompts without model calls. |
|
|
417
|
+
| `agentram_run_coding_orchestration` | Dry-run or execute endpoints concurrently. |
|
|
367
418
|
|
|
368
419
|
### Init
|
|
369
420
|
|
package/agentram/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from .adapter import AnthropicMemoryAdapter, HeuristicMemoryAdapter, OllamaMemoryAdapter, OpenAICompatibleMemoryAdapter
|
|
2
2
|
from .config import anthropic_from_env, load_env_file, merged_env, openai_compatible_from_env
|
|
3
3
|
from .daemon import AgentRAMDaemon
|
|
4
|
+
from .orchestration import AgentModelEndpoint, CodingOrchestrationRequest, MultiModelCodingOrchestrator
|
|
4
5
|
from .orchestrator import AgentRAM
|
|
5
6
|
from .retriever import MemoryRetriever
|
|
6
7
|
from .schema import Event, EventType, MemoryItem, MemoryPatch, MemoryScope, MemoryType
|
|
@@ -8,6 +9,8 @@ from .schema import Event, EventType, MemoryItem, MemoryPatch, MemoryScope, Memo
|
|
|
8
9
|
__all__ = [
|
|
9
10
|
"AgentRAM",
|
|
10
11
|
"AgentRAMDaemon",
|
|
12
|
+
"AgentModelEndpoint",
|
|
13
|
+
"CodingOrchestrationRequest",
|
|
11
14
|
"AnthropicMemoryAdapter",
|
|
12
15
|
"anthropic_from_env",
|
|
13
16
|
"Event",
|
|
@@ -20,6 +23,7 @@ __all__ = [
|
|
|
20
23
|
"MemoryRetriever",
|
|
21
24
|
"MemoryScope",
|
|
22
25
|
"MemoryType",
|
|
26
|
+
"MultiModelCodingOrchestrator",
|
|
23
27
|
"OllamaMemoryAdapter",
|
|
24
28
|
"openai_compatible_from_env",
|
|
25
29
|
"OpenAICompatibleMemoryAdapter",
|
package/agentram/cli.py
CHANGED
|
@@ -30,6 +30,9 @@ SLASH_HELP = """Slash commands:
|
|
|
30
30
|
/replay Replay events into memory
|
|
31
31
|
/agents List subagent presets
|
|
32
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
|
|
33
36
|
/claude-install [path] Install .claude commands and agents
|
|
34
37
|
/note <text> Save decision note
|
|
35
38
|
/clear Clear chat
|
|
@@ -48,6 +51,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
48
51
|
subparsers.add_parser("status", help="Show RAM file and item counts.")
|
|
49
52
|
subparsers.add_parser("goal", help="Show goal stack.")
|
|
50
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.")
|
|
51
68
|
|
|
52
69
|
retrieve = subparsers.add_parser("retrieve", help="Retrieve memory context.")
|
|
53
70
|
retrieve.add_argument("query", nargs="?", default="", help="Search query.")
|
|
@@ -111,6 +128,16 @@ def run_command(args: argparse.Namespace) -> int:
|
|
|
111
128
|
if command == "agents":
|
|
112
129
|
print(agents_text())
|
|
113
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
|
|
114
141
|
if command == "claude-install":
|
|
115
142
|
print(install_claude_templates_text(Path(args.target), force=args.force))
|
|
116
143
|
return 0
|
|
@@ -182,6 +209,19 @@ def handle_slash_command(context: McpContext, text: str, messages: list[str] | N
|
|
|
182
209
|
return replay_text(context, {})
|
|
183
210
|
if command == "/agents":
|
|
184
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)
|
|
185
225
|
if command == "/agent":
|
|
186
226
|
if not rest:
|
|
187
227
|
return "Usage: /agent <memory|planner|reviewer|documenter> <task>"
|
|
@@ -298,6 +338,55 @@ def agent_prompt_text(context: McpContext, name: str, task: str) -> str:
|
|
|
298
338
|
|
|
299
339
|
|
|
300
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
|
+
|
|
301
390
|
def claude_template_root() -> Path:
|
|
302
391
|
return Path(__file__).resolve().parent.parent / ".claude"
|
|
303
392
|
|
|
@@ -413,8 +502,9 @@ def sidebar_lines(context: McpContext) -> list[str]:
|
|
|
413
502
|
"",
|
|
414
503
|
"CLAUDE",
|
|
415
504
|
" /claude-install .",
|
|
416
|
-
" /
|
|
417
|
-
" /
|
|
505
|
+
" /models",
|
|
506
|
+
" /bind-model claude <model>",
|
|
507
|
+
" /orchestrate <task>",
|
|
418
508
|
]
|
|
419
509
|
|
|
420
510
|
|
package/agentram/mcp_server.py
CHANGED
|
@@ -11,9 +11,10 @@ from typing import Any
|
|
|
11
11
|
from .adapter import HeuristicMemoryAdapter
|
|
12
12
|
from .config import default_ram_root
|
|
13
13
|
from .merger import MemoryMerger
|
|
14
|
+
from .orchestration import AgentModelEndpoint, CodingOrchestrationRequest, MultiModelCodingOrchestrator
|
|
14
15
|
from .retriever import MemoryRetriever
|
|
15
16
|
from .schema import Event, EventType
|
|
16
|
-
from .storage import JsonGoalStore, JsonMemoryStore, JsonlEventLog, ensure_ram_storage
|
|
17
|
+
from .storage import JsonAgentProfileStore, JsonGoalStore, JsonMemoryStore, JsonlEventLog, ensure_ram_storage
|
|
17
18
|
|
|
18
19
|
JSONRPC_VERSION = "2.0"
|
|
19
20
|
SERVER_NAME = "agentram"
|
|
@@ -43,6 +44,10 @@ class McpContext:
|
|
|
43
44
|
def goal_store(self) -> JsonGoalStore:
|
|
44
45
|
return JsonGoalStore(self.ram_root / "goal_state.json")
|
|
45
46
|
|
|
47
|
+
@property
|
|
48
|
+
def profile_store(self) -> JsonAgentProfileStore:
|
|
49
|
+
return JsonAgentProfileStore(self.ram_root / "agent_profile.json")
|
|
50
|
+
|
|
46
51
|
@property
|
|
47
52
|
def merger(self) -> MemoryMerger:
|
|
48
53
|
return MemoryMerger(self.memory_store)
|
|
@@ -220,6 +225,60 @@ def tools_list() -> dict[str, Any]:
|
|
|
220
225
|
},
|
|
221
226
|
},
|
|
222
227
|
},
|
|
228
|
+
|
|
229
|
+
{
|
|
230
|
+
"name": "agentram_bind_coding_model",
|
|
231
|
+
"description": "Bind a coding-agent model endpoint that Claude or other agents can orchestrate later.",
|
|
232
|
+
"inputSchema": {
|
|
233
|
+
"type": "object",
|
|
234
|
+
"properties": {
|
|
235
|
+
"id": {"type": "string"},
|
|
236
|
+
"provider": {"type": "string", "default": "openai-compatible"},
|
|
237
|
+
"model": {"type": "string"},
|
|
238
|
+
"role": {"type": "string", "default": "coder"},
|
|
239
|
+
"base_url": {"type": "string"},
|
|
240
|
+
"api_key_env": {"type": "string"},
|
|
241
|
+
"modalities": {"type": "array", "items": {"type": "string"}},
|
|
242
|
+
"enabled": {"type": "boolean", "default": True},
|
|
243
|
+
},
|
|
244
|
+
"required": ["provider", "model"],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"name": "agentram_read_coding_models",
|
|
249
|
+
"description": "Read coding-agent model endpoints bound to this project RAM.",
|
|
250
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"name": "agentram_build_coding_orchestration",
|
|
254
|
+
"description": "Build prompts for all enabled coding model endpoints without calling models.",
|
|
255
|
+
"inputSchema": {
|
|
256
|
+
"type": "object",
|
|
257
|
+
"properties": {
|
|
258
|
+
"task": {"type": "string"},
|
|
259
|
+
"query": {"type": "string"},
|
|
260
|
+
"files": {"type": "array", "items": {"type": "string"}},
|
|
261
|
+
"modalities": {"type": "array", "items": {"type": "string"}},
|
|
262
|
+
},
|
|
263
|
+
"required": ["task"],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"name": "agentram_run_coding_orchestration",
|
|
268
|
+
"description": "Run enabled coding model endpoints concurrently when execute=true, otherwise return dry-run plan.",
|
|
269
|
+
"inputSchema": {
|
|
270
|
+
"type": "object",
|
|
271
|
+
"properties": {
|
|
272
|
+
"task": {"type": "string"},
|
|
273
|
+
"query": {"type": "string"},
|
|
274
|
+
"files": {"type": "array", "items": {"type": "string"}},
|
|
275
|
+
"modalities": {"type": "array", "items": {"type": "string"}},
|
|
276
|
+
"execute": {"type": "boolean", "default": False},
|
|
277
|
+
"max_workers": {"type": "integer", "default": 4},
|
|
278
|
+
},
|
|
279
|
+
"required": ["task"],
|
|
280
|
+
},
|
|
281
|
+
},
|
|
223
282
|
{
|
|
224
283
|
"name": "agentram_check_goal_drift",
|
|
225
284
|
"description": "Check whether a proposed task aligns with the current Goal Stack. Returns no suggestions when aligned.",
|
|
@@ -284,6 +343,32 @@ def call_tool(context: McpContext, name: str, arguments: dict[str, Any]) -> dict
|
|
|
284
343
|
state = context.goal_store.update(arguments)
|
|
285
344
|
return tool_result({"ok": True, "goal_state": state.to_dict(), "context": state.context_block()})
|
|
286
345
|
|
|
346
|
+
|
|
347
|
+
if name == "agentram_bind_coding_model":
|
|
348
|
+
endpoint = AgentModelEndpoint.from_dict(arguments)
|
|
349
|
+
data = context.profile_store.upsert(endpoint)
|
|
350
|
+
return tool_result({"ok": True, "endpoint": endpoint.to_dict(), "profile": data})
|
|
351
|
+
|
|
352
|
+
if name == "agentram_read_coding_models":
|
|
353
|
+
endpoints = context.profile_store.list_endpoints()
|
|
354
|
+
return tool_result({"endpoints": [endpoint.to_dict() for endpoint in endpoints]})
|
|
355
|
+
|
|
356
|
+
if name == "agentram_build_coding_orchestration":
|
|
357
|
+
request = orchestration_request_from_args(context, arguments)
|
|
358
|
+
endpoints = context.profile_store.list_endpoints()
|
|
359
|
+
plan = MultiModelCodingOrchestrator(endpoints).build_plan(request)
|
|
360
|
+
return tool_result({"ok": True, "execute": False, **plan})
|
|
361
|
+
|
|
362
|
+
if name == "agentram_run_coding_orchestration":
|
|
363
|
+
request = orchestration_request_from_args(context, arguments)
|
|
364
|
+
endpoints = context.profile_store.list_endpoints()
|
|
365
|
+
orchestrator = MultiModelCodingOrchestrator(endpoints)
|
|
366
|
+
if not bool(arguments.get("execute", False)):
|
|
367
|
+
plan = orchestrator.build_plan(request)
|
|
368
|
+
return tool_result({"ok": True, "execute": False, **plan})
|
|
369
|
+
result = orchestrator.run(request, max_workers=int(arguments.get("max_workers", 4)))
|
|
370
|
+
return tool_result({"ok": True, "execute": True, **result})
|
|
371
|
+
|
|
287
372
|
if name == "agentram_check_goal_drift":
|
|
288
373
|
task = str(arguments.get("task", ""))
|
|
289
374
|
if not task.strip():
|
|
@@ -293,6 +378,27 @@ def call_tool(context: McpContext, name: str, arguments: dict[str, Any]) -> dict
|
|
|
293
378
|
raise ValueError(f"unknown tool: {name}")
|
|
294
379
|
|
|
295
380
|
|
|
381
|
+
|
|
382
|
+
def orchestration_request_from_args(context: McpContext, arguments: dict[str, Any]) -> CodingOrchestrationRequest:
|
|
383
|
+
task = str(arguments.get("task", ""))
|
|
384
|
+
if not task.strip():
|
|
385
|
+
raise ValueError("task is required")
|
|
386
|
+
query = str(arguments.get("query") or task)
|
|
387
|
+
files = arguments.get("files", [])
|
|
388
|
+
modalities = arguments.get("modalities", ["text"])
|
|
389
|
+
if not isinstance(files, list):
|
|
390
|
+
raise ValueError("files must be an array")
|
|
391
|
+
if not isinstance(modalities, list):
|
|
392
|
+
raise ValueError("modalities must be an array")
|
|
393
|
+
retriever = MemoryRetriever(context.memory_store, context.goal_store)
|
|
394
|
+
context_block = retriever.context_block(query=query, task_id="codex", limit=8)
|
|
395
|
+
return CodingOrchestrationRequest(
|
|
396
|
+
task=task,
|
|
397
|
+
context=context_block,
|
|
398
|
+
files=[str(item) for item in files],
|
|
399
|
+
modalities=[str(item) for item in modalities],
|
|
400
|
+
)
|
|
401
|
+
|
|
296
402
|
def handle_request(context: McpContext, request: dict[str, Any]) -> dict[str, Any] | None:
|
|
297
403
|
request_id = request.get("id")
|
|
298
404
|
method = request.get("method")
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import urllib.request
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class AgentModelEndpoint:
|
|
14
|
+
provider: str
|
|
15
|
+
model: str
|
|
16
|
+
role: str = "coder"
|
|
17
|
+
base_url: str = ""
|
|
18
|
+
api_key_env: str = ""
|
|
19
|
+
modalities: list[str] = field(default_factory=lambda: ["text"])
|
|
20
|
+
enabled: bool = True
|
|
21
|
+
id: str = field(default_factory=lambda: f"model_{uuid4().hex[:10]}")
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict[str, Any]:
|
|
24
|
+
return {
|
|
25
|
+
"id": self.id,
|
|
26
|
+
"provider": self.provider,
|
|
27
|
+
"model": self.model,
|
|
28
|
+
"role": self.role,
|
|
29
|
+
"base_url": self.base_url,
|
|
30
|
+
"api_key_env": self.api_key_env,
|
|
31
|
+
"modalities": self.modalities,
|
|
32
|
+
"enabled": self.enabled,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_dict(cls, data: dict[str, Any]) -> "AgentModelEndpoint":
|
|
37
|
+
return cls(
|
|
38
|
+
id=str(data.get("id") or f"model_{uuid4().hex[:10]}"),
|
|
39
|
+
provider=str(data.get("provider", "openai-compatible")),
|
|
40
|
+
model=str(data.get("model", "")),
|
|
41
|
+
role=str(data.get("role", "coder")),
|
|
42
|
+
base_url=str(data.get("base_url", "")),
|
|
43
|
+
api_key_env=str(data.get("api_key_env", "")),
|
|
44
|
+
modalities=[str(item) for item in data.get("modalities", ["text"])],
|
|
45
|
+
enabled=bool(data.get("enabled", True)),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class CodingOrchestrationRequest:
|
|
51
|
+
task: str
|
|
52
|
+
context: str = ""
|
|
53
|
+
files: list[str] = field(default_factory=list)
|
|
54
|
+
modalities: list[str] = field(default_factory=lambda: ["text"])
|
|
55
|
+
|
|
56
|
+
def to_prompt(self, endpoint: AgentModelEndpoint) -> str:
|
|
57
|
+
return "\n".join(
|
|
58
|
+
[
|
|
59
|
+
f"You are AgentRAM {endpoint.role} agent.",
|
|
60
|
+
f"Model: {endpoint.model}",
|
|
61
|
+
f"Provider: {endpoint.provider}",
|
|
62
|
+
f"Modalities: {', '.join(endpoint.modalities)}",
|
|
63
|
+
"Task:",
|
|
64
|
+
self.task,
|
|
65
|
+
"Files:",
|
|
66
|
+
"\n".join(f"- {item}" for item in self.files) or "- none provided",
|
|
67
|
+
"AgentRAM Context:",
|
|
68
|
+
self.context or "No context provided.",
|
|
69
|
+
"Output contract:",
|
|
70
|
+
"- Return concise coding findings or patch plan.",
|
|
71
|
+
"- Mention files to inspect or change.",
|
|
72
|
+
"- Do not invent repo facts.",
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class MultiModelCodingOrchestrator:
|
|
78
|
+
def __init__(self, endpoints: list[AgentModelEndpoint], client: Callable[[AgentModelEndpoint, str], str] | None = None) -> None:
|
|
79
|
+
self.endpoints = [endpoint for endpoint in endpoints if endpoint.enabled]
|
|
80
|
+
self.client = client or self._call_endpoint
|
|
81
|
+
|
|
82
|
+
def build_plan(self, request: CodingOrchestrationRequest) -> dict[str, Any]:
|
|
83
|
+
agents = []
|
|
84
|
+
for endpoint in self.endpoints:
|
|
85
|
+
agents.append({"endpoint": endpoint.to_dict(), "prompt": request.to_prompt(endpoint)})
|
|
86
|
+
return {"task": request.task, "agent_count": len(agents), "agents": agents}
|
|
87
|
+
|
|
88
|
+
def run(self, request: CodingOrchestrationRequest, max_workers: int = 4) -> dict[str, Any]:
|
|
89
|
+
results: list[dict[str, Any]] = []
|
|
90
|
+
if not self.endpoints:
|
|
91
|
+
return {"task": request.task, "agent_count": 0, "results": []}
|
|
92
|
+
with ThreadPoolExecutor(max_workers=max(1, min(max_workers, len(self.endpoints)))) as executor:
|
|
93
|
+
future_map = {executor.submit(self.client, endpoint, request.to_prompt(endpoint)): endpoint for endpoint in self.endpoints}
|
|
94
|
+
for future in as_completed(future_map):
|
|
95
|
+
endpoint = future_map[future]
|
|
96
|
+
try:
|
|
97
|
+
output = future.result()
|
|
98
|
+
results.append({"endpoint": endpoint.to_dict(), "ok": True, "output": output})
|
|
99
|
+
except Exception as error: # noqa: BLE001 - agent boundary
|
|
100
|
+
results.append({"endpoint": endpoint.to_dict(), "ok": False, "error": str(error)})
|
|
101
|
+
return {"task": request.task, "agent_count": len(self.endpoints), "results": results}
|
|
102
|
+
|
|
103
|
+
def _call_endpoint(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
|
|
104
|
+
provider = endpoint.provider.lower()
|
|
105
|
+
if provider in {"openai", "openai-compatible", "groq", "vllm", "lmstudio", "ollama-v1"}:
|
|
106
|
+
return self._call_openai_compatible(endpoint, prompt)
|
|
107
|
+
if provider in {"anthropic", "claude"}:
|
|
108
|
+
return self._call_anthropic(endpoint, prompt)
|
|
109
|
+
raise ValueError(f"Unsupported provider for execution: {endpoint.provider}")
|
|
110
|
+
|
|
111
|
+
def _call_openai_compatible(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
|
|
112
|
+
api_key = os.getenv(endpoint.api_key_env) if endpoint.api_key_env else ""
|
|
113
|
+
if not endpoint.base_url:
|
|
114
|
+
raise ValueError("base_url is required")
|
|
115
|
+
if endpoint.api_key_env and not api_key:
|
|
116
|
+
raise ValueError(f"missing API key env: {endpoint.api_key_env}")
|
|
117
|
+
payload = {"model": endpoint.model, "messages": [{"role": "user", "content": prompt}], "temperature": 0}
|
|
118
|
+
request = urllib.request.Request(
|
|
119
|
+
endpoint.base_url.rstrip("/") + "/chat/completions",
|
|
120
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
121
|
+
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"} if api_key else {"Content-Type": "application/json"},
|
|
122
|
+
method="POST",
|
|
123
|
+
)
|
|
124
|
+
with urllib.request.urlopen(request, timeout=120) as response:
|
|
125
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
126
|
+
return str(data.get("choices", [{}])[0].get("message", {}).get("content", ""))
|
|
127
|
+
|
|
128
|
+
def _call_anthropic(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
|
|
129
|
+
api_key = os.getenv(endpoint.api_key_env) if endpoint.api_key_env else ""
|
|
130
|
+
if endpoint.api_key_env and not api_key:
|
|
131
|
+
raise ValueError(f"missing API key env: {endpoint.api_key_env}")
|
|
132
|
+
base_url = endpoint.base_url.rstrip("/") if endpoint.base_url else "https://api.anthropic.com/v1"
|
|
133
|
+
payload = {"model": endpoint.model, "max_tokens": 1024, "messages": [{"role": "user", "content": prompt}]}
|
|
134
|
+
request = urllib.request.Request(
|
|
135
|
+
base_url + "/messages",
|
|
136
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
137
|
+
headers={"Content-Type": "application/json", "x-api-key": api_key, "anthropic-version": "2023-06-01"},
|
|
138
|
+
method="POST",
|
|
139
|
+
)
|
|
140
|
+
with urllib.request.urlopen(request, timeout=120) as response:
|
|
141
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
142
|
+
content = data.get("content", [])
|
|
143
|
+
if content and isinstance(content, list):
|
|
144
|
+
return "\n".join(str(item.get("text", "")) for item in content if isinstance(item, dict))
|
|
145
|
+
return str(data)
|
package/agentram/storage.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
from .orchestration import AgentModelEndpoint
|
|
6
7
|
from .schema import Event, GoalState, MemoryItem, utc_now
|
|
7
8
|
|
|
8
9
|
|
|
@@ -74,6 +75,40 @@ class JsonGoalStore:
|
|
|
74
75
|
return self.save(GoalState.from_dict(current))
|
|
75
76
|
|
|
76
77
|
|
|
78
|
+
class JsonAgentProfileStore:
|
|
79
|
+
def __init__(self, path: str | Path) -> None:
|
|
80
|
+
self.path = Path(path)
|
|
81
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
|
|
83
|
+
def load(self) -> dict[str, object]:
|
|
84
|
+
if not self.path.exists():
|
|
85
|
+
return {"updated_at": utc_now(), "endpoints": []}
|
|
86
|
+
return json.loads(self.path.read_text(encoding="utf-8"))
|
|
87
|
+
|
|
88
|
+
def save(self, endpoints: list[AgentModelEndpoint]) -> dict[str, object]:
|
|
89
|
+
payload = {"updated_at": utc_now(), "endpoints": [endpoint.to_dict() for endpoint in endpoints]}
|
|
90
|
+
self.path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
91
|
+
return payload
|
|
92
|
+
|
|
93
|
+
def list_endpoints(self) -> list[AgentModelEndpoint]:
|
|
94
|
+
data = self.load()
|
|
95
|
+
return [AgentModelEndpoint.from_dict(item) for item in data.get("endpoints", []) if isinstance(item, dict)]
|
|
96
|
+
|
|
97
|
+
def upsert(self, endpoint: AgentModelEndpoint) -> dict[str, object]:
|
|
98
|
+
endpoints = self.list_endpoints()
|
|
99
|
+
replaced = False
|
|
100
|
+
updated: list[AgentModelEndpoint] = []
|
|
101
|
+
for current in endpoints:
|
|
102
|
+
if current.id == endpoint.id:
|
|
103
|
+
updated.append(endpoint)
|
|
104
|
+
replaced = True
|
|
105
|
+
else:
|
|
106
|
+
updated.append(current)
|
|
107
|
+
if not replaced:
|
|
108
|
+
updated.append(endpoint)
|
|
109
|
+
return self.save(updated)
|
|
110
|
+
|
|
111
|
+
|
|
77
112
|
def ensure_ram_storage(root: str | Path) -> dict[str, object]:
|
|
78
113
|
ram_root = Path(root)
|
|
79
114
|
ram_root.mkdir(parents=True, exist_ok=True)
|
|
@@ -81,6 +116,7 @@ def ensure_ram_storage(root: str | Path) -> dict[str, object]:
|
|
|
81
116
|
events_path = ram_root / "events.jsonl"
|
|
82
117
|
memory_path = ram_root / "memory.json"
|
|
83
118
|
goal_path = ram_root / "goal_state.json"
|
|
119
|
+
profile_path = ram_root / "agent_profile.json"
|
|
84
120
|
|
|
85
121
|
created: list[str] = []
|
|
86
122
|
if not events_path.exists():
|
|
@@ -92,6 +128,9 @@ def ensure_ram_storage(root: str | Path) -> dict[str, object]:
|
|
|
92
128
|
if not goal_path.exists():
|
|
93
129
|
JsonGoalStore(goal_path).save(GoalState())
|
|
94
130
|
created.append("goal_state.json")
|
|
131
|
+
if not profile_path.exists():
|
|
132
|
+
JsonAgentProfileStore(profile_path).save([])
|
|
133
|
+
created.append("agent_profile.json")
|
|
95
134
|
|
|
96
135
|
return {
|
|
97
136
|
"ram_root": str(ram_root),
|
|
@@ -100,5 +139,6 @@ def ensure_ram_storage(root: str | Path) -> dict[str, object]:
|
|
|
100
139
|
"events": str(events_path),
|
|
101
140
|
"memory": str(memory_path),
|
|
102
141
|
"goal_state": str(goal_path),
|
|
142
|
+
"agent_profile": str(profile_path),
|
|
103
143
|
},
|
|
104
144
|
}
|
package/package.json
CHANGED