aru-code 0.25.2__tar.gz → 0.26.0__tar.gz
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.
- {aru_code-0.25.2/aru_code.egg-info → aru_code-0.26.0}/PKG-INFO +158 -20
- {aru_code-0.25.2 → aru_code-0.26.0}/README.md +157 -19
- aru_code-0.26.0/aru/__init__.py +1 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/agent_factory.py +82 -22
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/agents/base.py +67 -2
- aru_code-0.26.0/aru/agents/catalog.py +84 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/agents/planner.py +1 -40
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/cli.py +15 -29
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/runner.py +124 -292
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/session.py +4 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/codebase.py +51 -6
- aru_code-0.26.0/aru/tools/plan_mode.py +65 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/tasklist.py +74 -0
- {aru_code-0.25.2 → aru_code-0.26.0/aru_code.egg-info}/PKG-INFO +158 -20
- {aru_code-0.25.2 → aru_code-0.26.0}/aru_code.egg-info/SOURCES.txt +3 -4
- {aru_code-0.25.2 → aru_code-0.26.0}/pyproject.toml +9 -1
- aru_code-0.26.0/tests/test_catalog.py +95 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_guardrails_scenarios.py +6 -0
- aru_code-0.25.2/aru/__init__.py +0 -1
- aru_code-0.25.2/aru/agents/executor.py +0 -59
- aru_code-0.25.2/aru/agents/explorer.py +0 -93
- aru_code-0.25.2/tests/test_executor.py +0 -81
- aru_code-0.25.2/tests/test_planner.py +0 -107
- {aru_code-0.25.2 → aru_code-0.26.0}/LICENSE +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/cache_patch.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/checkpoints.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/commands.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/completers.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/config.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/context.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/display.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/history_blocks.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/permissions.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/plugins/__init__.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/plugins/custom_tools.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/plugins/hooks.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/plugins/manager.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/plugins/tool_api.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/providers.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/runtime.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/setup.cfg +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_checkpoints.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_codebase.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_confabulation_regression.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_config.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_context.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_main.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_permissions.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_plugins.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_providers.py +0 -0
- {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_ranker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aru-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.0
|
|
4
4
|
Summary: A Claude Code clone built with Agno agents
|
|
5
5
|
Author-email: Estevao <estevaofon@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -49,17 +49,22 @@ Dynamic: license-file
|
|
|
49
49
|
|
|
50
50
|
An intelligent coding assistant for the terminal, powered by LLMs and [Agno](https://github.com/agno-agi/agno) agents.
|
|
51
51
|
|
|
52
|
+
📖 **Full documentation:** [https://estevaofon.github.io/aru/](https://estevaofon.github.io/aru/)
|
|
53
|
+
|
|
52
54
|

|
|
53
55
|
|
|
54
56
|
## Highlights
|
|
55
57
|
|
|
56
|
-
- **Multi-Agent Architecture** —
|
|
58
|
+
- **Catalog-Driven Multi-Agent Architecture** — `build`, `plan`, `executor`, and `explorer` (subagent) specs resolved from a single source of truth (`aru/agents/catalog.py`)
|
|
59
|
+
- **Autonomous Plan Mode** — Agents self-trigger planning via `enter_plan_mode(task)`; plan steps are persisted in the session and surfaced each turn as a `PLAN ACTIVE` reminder
|
|
60
|
+
- **Structured Subtask Tracking** — `create_task_list` / `update_task` / `update_plan_step` force the executor to plan, execute, and mark subtasks as it goes
|
|
57
61
|
- **Interactive CLI** — Streaming responses, multi-line paste, session management
|
|
58
62
|
- **Image Support** — Attach images via `@` mentions for multimodal analysis (Claude, GPT-4o, Gemini)
|
|
59
|
-
- **
|
|
60
|
-
- **Task Planning** — Break down complex tasks into steps with automatic execution
|
|
63
|
+
- **17 Integrated Tools** — File I/O (single + batched), code search, shell, web, delegation, plan/task tracking
|
|
61
64
|
- **Multi-Provider** — Anthropic, OpenAI, Ollama, Groq, OpenRouter, DeepSeek, and others via custom configuration
|
|
62
65
|
- **Custom Commands, Skills, and Agents** — Extend aru via the `.agents/` directory
|
|
66
|
+
- **Custom Tools** — Add your own Python tools with a simple `@tool` decorator
|
|
67
|
+
- **Plugin System** — OpenCode-compatible hooks for tool lifecycle, chat, permissions, and more
|
|
63
68
|
- **MCP Support** — Integration with Model Context Protocol servers
|
|
64
69
|
|
|
65
70
|
## Quick Start
|
|
@@ -491,6 +496,116 @@ Each agent gets its own isolated "always" memory — approvals during an agent's
|
|
|
491
496
|
|
|
492
497
|
Agents with `mode: subagent` can be referenced by the LLM via `delegate_task(task, agent="name")` but are not directly invocable from the CLI.
|
|
493
498
|
|
|
499
|
+
### Custom Tools
|
|
500
|
+
|
|
501
|
+
You can extend aru with your own Python tools. Drop a `.py` file in `.aru/tools/` (project) or `~/.aru/tools/` (global) — aru auto-discovers and registers every function found.
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
# .aru/tools/deploy.py
|
|
505
|
+
from aru.plugins import tool
|
|
506
|
+
|
|
507
|
+
@tool(description="Deploy the current branch to an environment")
|
|
508
|
+
def deploy(environment: str = "staging") -> str:
|
|
509
|
+
"""Runs the deploy script and returns the output."""
|
|
510
|
+
import subprocess
|
|
511
|
+
result = subprocess.run(
|
|
512
|
+
["./scripts/deploy.sh", environment],
|
|
513
|
+
capture_output=True, text=True,
|
|
514
|
+
)
|
|
515
|
+
return result.stdout or result.stderr
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
The LLM sees each tool as a first-class function — name, description, and typed parameters are inferred from the signature.
|
|
519
|
+
|
|
520
|
+
#### Rules
|
|
521
|
+
|
|
522
|
+
- **Decorator is optional.** A bare `def fn(...) -> str` with a docstring works too. Use `@tool(...)` when you want a custom description or to override a built-in.
|
|
523
|
+
- **Parameters** are read from type hints; defaults become optional params.
|
|
524
|
+
- **Return type** should be `str` (or something stringifiable) — the result is sent back to the LLM as tool output.
|
|
525
|
+
- **Override built-ins** with `@tool(override=True)` if you want to replace, say, `bash` with your own implementation.
|
|
526
|
+
- **Discovery paths** (later roots override earlier ones):
|
|
527
|
+
1. `~/.aru/tools/`
|
|
528
|
+
2. `.aru/tools/`
|
|
529
|
+
3. `~/.agents/tools/`
|
|
530
|
+
4. `.agents/tools/`
|
|
531
|
+
|
|
532
|
+
Both sync and `async def` functions are supported.
|
|
533
|
+
|
|
534
|
+
### Plugins
|
|
535
|
+
|
|
536
|
+
For more control than custom tools — e.g. intercepting tool calls, mutating chat messages, injecting env vars into shell commands, or blocking permissions — use the plugin system. Plugins are Python files that return a `Hooks` object, mirroring OpenCode's hook pattern.
|
|
537
|
+
|
|
538
|
+
```python
|
|
539
|
+
# .aru/plugins/audit.py
|
|
540
|
+
from aru.plugins import Hooks, PluginInput
|
|
541
|
+
|
|
542
|
+
async def plugin(ctx: PluginInput, options: dict | None = None) -> Hooks:
|
|
543
|
+
hooks = Hooks()
|
|
544
|
+
|
|
545
|
+
@hooks.on("tool.execute.before")
|
|
546
|
+
async def before_tool(event):
|
|
547
|
+
print(f"[audit] running {event.tool_name} with {event.args}")
|
|
548
|
+
|
|
549
|
+
@hooks.on("tool.execute.after")
|
|
550
|
+
async def after_tool(event):
|
|
551
|
+
print(f"[audit] {event.tool_name} → ok")
|
|
552
|
+
|
|
553
|
+
@hooks.on("shell.env")
|
|
554
|
+
async def inject_env(event):
|
|
555
|
+
event.env["DEPLOY_TOKEN"] = "••••"
|
|
556
|
+
|
|
557
|
+
# You can also register tools directly from a plugin:
|
|
558
|
+
def greet(name: str) -> str:
|
|
559
|
+
"""Say hello."""
|
|
560
|
+
return f"hello, {name}"
|
|
561
|
+
hooks.tools["greet"] = greet
|
|
562
|
+
|
|
563
|
+
return hooks
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Save the file as `.aru/plugins/<name>.py` and aru will load it automatically at startup.
|
|
567
|
+
|
|
568
|
+
#### Available hooks
|
|
569
|
+
|
|
570
|
+
| Hook | When it fires | Typical use |
|
|
571
|
+
|------|---------------|-------------|
|
|
572
|
+
| `config` | After config is loaded | Read/adjust config |
|
|
573
|
+
| `tool.execute.before` | Before any tool runs | Audit, block, mutate args |
|
|
574
|
+
| `tool.execute.after` | After any tool runs | Log, post-process results |
|
|
575
|
+
| `tool.definition` | When tool list is resolved | Modify tool descriptions/params |
|
|
576
|
+
| `chat.message` | Before a user message is sent to the LLM | Rewrite the message |
|
|
577
|
+
| `chat.params` | Before the LLM call | Adjust `temperature`, `max_tokens` |
|
|
578
|
+
| `chat.system.transform` | Before the LLM call | Modify the system prompt |
|
|
579
|
+
| `chat.messages.transform` | Before the LLM call | Modify the full message history |
|
|
580
|
+
| `command.execute.before` | Before a slash command runs | Block or rewrite commands |
|
|
581
|
+
| `permission.ask` | Before a permission prompt | Auto-allow/deny |
|
|
582
|
+
| `shell.env` | Before `bash` runs | Inject env vars |
|
|
583
|
+
| `session.compact` | Before context compaction | React to compaction |
|
|
584
|
+
| `event` | Any published event | Generic subscription |
|
|
585
|
+
|
|
586
|
+
Handlers can be sync or `async`. They run sequentially so each can mutate the event before the next handler sees it. Raise `PermissionError` to block an action.
|
|
587
|
+
|
|
588
|
+
#### Loading plugins
|
|
589
|
+
|
|
590
|
+
Plugins come from three sources:
|
|
591
|
+
|
|
592
|
+
1. **Auto-discovery** — `.aru/plugins/*.py`, `.agents/plugins/*.py`, and the same paths under `~/`
|
|
593
|
+
2. **Config** — explicit list in `aru.json`:
|
|
594
|
+
|
|
595
|
+
```json
|
|
596
|
+
{
|
|
597
|
+
"plugins": [
|
|
598
|
+
"my-package-plugin",
|
|
599
|
+
["./.aru/plugins/audit.py", { "verbose": true }]
|
|
600
|
+
]
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
The second form passes options to the plugin as the `options` argument.
|
|
605
|
+
3. **Entry points** — installed packages can register via the `aru.plugins` entry point group
|
|
606
|
+
|
|
607
|
+
Every plugin file must export a `plugin(ctx, options)` function (sync or async) that returns a `Hooks` instance.
|
|
608
|
+
|
|
494
609
|
### MCP Support (Model Context Protocol)
|
|
495
610
|
|
|
496
611
|
Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
@@ -508,33 +623,53 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
|
508
623
|
|
|
509
624
|
## Agents
|
|
510
625
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
|
514
|
-
|
|
515
|
-
|
|
|
516
|
-
|
|
|
626
|
+
Built-in agents are declared as specs in `aru/agents/catalog.py` and instantiated on demand by `agent_factory.create_agent_from_spec`. A single construction path resolves the model, tool list, prompt role, and plugin hooks for all native agents.
|
|
627
|
+
|
|
628
|
+
| Agent | Mode | Role | Tools |
|
|
629
|
+
|-------|------|------|-------|
|
|
630
|
+
| **`build`** (General) | primary | Conversational coding assistant. Self-triggers `enter_plan_mode` for 3+ file changes | Full tool set including `delegate_task` |
|
|
631
|
+
| **`plan`** (Planner) | primary | Read-only analysis → `## Summary` + `## Steps` markdown plan | Read/search only (`read_file`, `read_files`, `glob_search`, `grep_search`, `list_directory`) |
|
|
632
|
+
| **`executor`** | primary | Step-by-step execution of a stored plan with mandatory task list tracking | Full tool set |
|
|
633
|
+
| **`explorer`** | **subagent** | Fast, read-only codebase research. Invoked only via `delegate_task(task, agent_name="explorer")` | Read/search + read-only `bash` + `rank_files` |
|
|
634
|
+
|
|
635
|
+
> **Scope reviewer:** `aru/agents/planner.py` also exposes `review_plan(request, plan)`, a one-shot, no-tool reviewer that runs on the small model to trim scope creep from generated plans. Enabled via `plan_reviewer: true` in `aru.json`.
|
|
636
|
+
|
|
637
|
+
### Plan mode flow
|
|
638
|
+
|
|
639
|
+
The `plan` agent runs in two ways:
|
|
640
|
+
|
|
641
|
+
1. **Manual:** the user types `/plan <task>` — the planner produces a plan, the reviewer optionally trims it, and the result is stored in the session.
|
|
642
|
+
2. **Autonomous:** the `build` agent calls `enter_plan_mode(task)` when it detects a multi-file task. This invokes the planner, stores the plan, and returns a summary.
|
|
643
|
+
|
|
644
|
+
Once a plan is stored, every following turn prepends a `<system-reminder>` listing all plan steps with their status icons. The build/executor agent works through them in order, calling `update_plan_step(index, "completed")` after each. Within a step, it calls `create_task_list([...])` to break the step into 1–10 concrete subtasks, then `update_task(i, "completed")` as they finish.
|
|
517
645
|
|
|
518
646
|
## Tools
|
|
519
647
|
|
|
520
648
|
### File Operations
|
|
521
649
|
- `read_file` — Reads files with line range support and binary detection
|
|
522
|
-
- `read_files` — Reads multiple files in parallel (
|
|
650
|
+
- `read_files` — Reads multiple files in parallel (batched)
|
|
523
651
|
- `write_file` — Writes content to files, creating directories as needed
|
|
652
|
+
- `write_files` — Writes multiple files in one call
|
|
524
653
|
- `edit_file` — Find-and-replace edits on files
|
|
654
|
+
- `edit_files` — Batched find-and-replace across multiple files
|
|
525
655
|
|
|
526
656
|
### Search & Discovery
|
|
527
657
|
- `glob_search` — Find files by pattern (respects .gitignore)
|
|
528
658
|
- `grep_search` — Content search with regex and file filtering
|
|
529
659
|
- `list_directory` — Directory listing with gitignore filtering
|
|
660
|
+
- `rank_files` — Multi-factor file relevance ranking (explorer subagent only)
|
|
530
661
|
|
|
531
662
|
### Shell & Web
|
|
532
663
|
- `bash` — Executes shell commands with permission gates
|
|
533
664
|
- `web_search` — Web search via DuckDuckGo
|
|
534
665
|
- `web_fetch` — Fetches URLs and converts HTML to readable text
|
|
535
666
|
|
|
536
|
-
###
|
|
537
|
-
- `
|
|
667
|
+
### Planning & Delegation
|
|
668
|
+
- `enter_plan_mode` — Generate a structured plan via the planner agent and store it in the session
|
|
669
|
+
- `update_plan_step` — Mark a macro plan step as `in_progress` / `completed` / `failed` / `skipped`
|
|
670
|
+
- `create_task_list` — Declare 1–10 subtasks for the current step (mandatory first executor call)
|
|
671
|
+
- `update_task` — Mark a subtask as `in_progress` / `completed` / `failed`
|
|
672
|
+
- `delegate_task` — Spawn an autonomous subagent (defaults to `explorer`) for parallel research or execution
|
|
538
673
|
|
|
539
674
|
## Architecture
|
|
540
675
|
|
|
@@ -542,22 +677,25 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
|
542
677
|
aru-code/
|
|
543
678
|
├── aru/
|
|
544
679
|
│ ├── cli.py # Main REPL loop, argument parsing, and entry point
|
|
545
|
-
│ ├── agent_factory.py #
|
|
680
|
+
│ ├── agent_factory.py # Single factory — builds Agno Agents from catalog specs
|
|
546
681
|
│ ├── commands.py # Slash commands, help display, shell execution
|
|
547
682
|
│ ├── completers.py # Input completions, paste detection, @file mentions
|
|
548
683
|
│ ├── context.py # Token optimization (pruning, truncation, compaction)
|
|
549
684
|
│ ├── display.py # Terminal display (logo, status bar, streaming output)
|
|
550
|
-
│ ├── runner.py # Agent execution
|
|
551
|
-
│ ├── session.py # Session state, persistence, plan tracking
|
|
685
|
+
│ ├── runner.py # Agent execution, streaming, PLAN ACTIVE reminder injection
|
|
686
|
+
│ ├── session.py # Session state, persistence, plan steps tracking
|
|
687
|
+
│ ├── runtime.py # Request context (TaskStore, session, display handles)
|
|
552
688
|
│ ├── config.py # Configuration loader (AGENTS.md, .agents/)
|
|
553
689
|
│ ├── providers.py # Multi-provider LLM abstraction
|
|
554
690
|
│ ├── permissions.py # Granular permission system (allow/ask/deny)
|
|
555
691
|
│ ├── agents/
|
|
556
|
-
│ │ ├──
|
|
557
|
-
│ │ ├──
|
|
558
|
-
│ │ └──
|
|
692
|
+
│ │ ├── base.py # Shared prompt templates + build_instructions(role)
|
|
693
|
+
│ │ ├── catalog.py # AgentSpec registry — build / plan / executor / explorer
|
|
694
|
+
│ │ └── planner.py # review_plan() — small-model scope reviewer
|
|
559
695
|
│ └── tools/
|
|
560
|
-
│ ├── codebase.py #
|
|
696
|
+
│ ├── codebase.py # Core tool implementations + GENERAL/EXECUTOR/PLANNER/EXPLORER sets
|
|
697
|
+
│ ├── plan_mode.py # enter_plan_mode tool (agent-invokable planner entry)
|
|
698
|
+
│ ├── tasklist.py # create_task_list / update_task / update_plan_step
|
|
561
699
|
│ ├── ast_tools.py # Tree-sitter code analysis
|
|
562
700
|
│ ├── ranker.py # File relevance ranking
|
|
563
701
|
│ ├── mcp_client.py # MCP client
|
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
An intelligent coding assistant for the terminal, powered by LLMs and [Agno](https://github.com/agno-agi/agno) agents.
|
|
4
4
|
|
|
5
|
+
📖 **Full documentation:** [https://estevaofon.github.io/aru/](https://estevaofon.github.io/aru/)
|
|
6
|
+
|
|
5
7
|

|
|
6
8
|
|
|
7
9
|
## Highlights
|
|
8
10
|
|
|
9
|
-
- **Multi-Agent Architecture** —
|
|
11
|
+
- **Catalog-Driven Multi-Agent Architecture** — `build`, `plan`, `executor`, and `explorer` (subagent) specs resolved from a single source of truth (`aru/agents/catalog.py`)
|
|
12
|
+
- **Autonomous Plan Mode** — Agents self-trigger planning via `enter_plan_mode(task)`; plan steps are persisted in the session and surfaced each turn as a `PLAN ACTIVE` reminder
|
|
13
|
+
- **Structured Subtask Tracking** — `create_task_list` / `update_task` / `update_plan_step` force the executor to plan, execute, and mark subtasks as it goes
|
|
10
14
|
- **Interactive CLI** — Streaming responses, multi-line paste, session management
|
|
11
15
|
- **Image Support** — Attach images via `@` mentions for multimodal analysis (Claude, GPT-4o, Gemini)
|
|
12
|
-
- **
|
|
13
|
-
- **Task Planning** — Break down complex tasks into steps with automatic execution
|
|
16
|
+
- **17 Integrated Tools** — File I/O (single + batched), code search, shell, web, delegation, plan/task tracking
|
|
14
17
|
- **Multi-Provider** — Anthropic, OpenAI, Ollama, Groq, OpenRouter, DeepSeek, and others via custom configuration
|
|
15
18
|
- **Custom Commands, Skills, and Agents** — Extend aru via the `.agents/` directory
|
|
19
|
+
- **Custom Tools** — Add your own Python tools with a simple `@tool` decorator
|
|
20
|
+
- **Plugin System** — OpenCode-compatible hooks for tool lifecycle, chat, permissions, and more
|
|
16
21
|
- **MCP Support** — Integration with Model Context Protocol servers
|
|
17
22
|
|
|
18
23
|
## Quick Start
|
|
@@ -444,6 +449,116 @@ Each agent gets its own isolated "always" memory — approvals during an agent's
|
|
|
444
449
|
|
|
445
450
|
Agents with `mode: subagent` can be referenced by the LLM via `delegate_task(task, agent="name")` but are not directly invocable from the CLI.
|
|
446
451
|
|
|
452
|
+
### Custom Tools
|
|
453
|
+
|
|
454
|
+
You can extend aru with your own Python tools. Drop a `.py` file in `.aru/tools/` (project) or `~/.aru/tools/` (global) — aru auto-discovers and registers every function found.
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
# .aru/tools/deploy.py
|
|
458
|
+
from aru.plugins import tool
|
|
459
|
+
|
|
460
|
+
@tool(description="Deploy the current branch to an environment")
|
|
461
|
+
def deploy(environment: str = "staging") -> str:
|
|
462
|
+
"""Runs the deploy script and returns the output."""
|
|
463
|
+
import subprocess
|
|
464
|
+
result = subprocess.run(
|
|
465
|
+
["./scripts/deploy.sh", environment],
|
|
466
|
+
capture_output=True, text=True,
|
|
467
|
+
)
|
|
468
|
+
return result.stdout or result.stderr
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The LLM sees each tool as a first-class function — name, description, and typed parameters are inferred from the signature.
|
|
472
|
+
|
|
473
|
+
#### Rules
|
|
474
|
+
|
|
475
|
+
- **Decorator is optional.** A bare `def fn(...) -> str` with a docstring works too. Use `@tool(...)` when you want a custom description or to override a built-in.
|
|
476
|
+
- **Parameters** are read from type hints; defaults become optional params.
|
|
477
|
+
- **Return type** should be `str` (or something stringifiable) — the result is sent back to the LLM as tool output.
|
|
478
|
+
- **Override built-ins** with `@tool(override=True)` if you want to replace, say, `bash` with your own implementation.
|
|
479
|
+
- **Discovery paths** (later roots override earlier ones):
|
|
480
|
+
1. `~/.aru/tools/`
|
|
481
|
+
2. `.aru/tools/`
|
|
482
|
+
3. `~/.agents/tools/`
|
|
483
|
+
4. `.agents/tools/`
|
|
484
|
+
|
|
485
|
+
Both sync and `async def` functions are supported.
|
|
486
|
+
|
|
487
|
+
### Plugins
|
|
488
|
+
|
|
489
|
+
For more control than custom tools — e.g. intercepting tool calls, mutating chat messages, injecting env vars into shell commands, or blocking permissions — use the plugin system. Plugins are Python files that return a `Hooks` object, mirroring OpenCode's hook pattern.
|
|
490
|
+
|
|
491
|
+
```python
|
|
492
|
+
# .aru/plugins/audit.py
|
|
493
|
+
from aru.plugins import Hooks, PluginInput
|
|
494
|
+
|
|
495
|
+
async def plugin(ctx: PluginInput, options: dict | None = None) -> Hooks:
|
|
496
|
+
hooks = Hooks()
|
|
497
|
+
|
|
498
|
+
@hooks.on("tool.execute.before")
|
|
499
|
+
async def before_tool(event):
|
|
500
|
+
print(f"[audit] running {event.tool_name} with {event.args}")
|
|
501
|
+
|
|
502
|
+
@hooks.on("tool.execute.after")
|
|
503
|
+
async def after_tool(event):
|
|
504
|
+
print(f"[audit] {event.tool_name} → ok")
|
|
505
|
+
|
|
506
|
+
@hooks.on("shell.env")
|
|
507
|
+
async def inject_env(event):
|
|
508
|
+
event.env["DEPLOY_TOKEN"] = "••••"
|
|
509
|
+
|
|
510
|
+
# You can also register tools directly from a plugin:
|
|
511
|
+
def greet(name: str) -> str:
|
|
512
|
+
"""Say hello."""
|
|
513
|
+
return f"hello, {name}"
|
|
514
|
+
hooks.tools["greet"] = greet
|
|
515
|
+
|
|
516
|
+
return hooks
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Save the file as `.aru/plugins/<name>.py` and aru will load it automatically at startup.
|
|
520
|
+
|
|
521
|
+
#### Available hooks
|
|
522
|
+
|
|
523
|
+
| Hook | When it fires | Typical use |
|
|
524
|
+
|------|---------------|-------------|
|
|
525
|
+
| `config` | After config is loaded | Read/adjust config |
|
|
526
|
+
| `tool.execute.before` | Before any tool runs | Audit, block, mutate args |
|
|
527
|
+
| `tool.execute.after` | After any tool runs | Log, post-process results |
|
|
528
|
+
| `tool.definition` | When tool list is resolved | Modify tool descriptions/params |
|
|
529
|
+
| `chat.message` | Before a user message is sent to the LLM | Rewrite the message |
|
|
530
|
+
| `chat.params` | Before the LLM call | Adjust `temperature`, `max_tokens` |
|
|
531
|
+
| `chat.system.transform` | Before the LLM call | Modify the system prompt |
|
|
532
|
+
| `chat.messages.transform` | Before the LLM call | Modify the full message history |
|
|
533
|
+
| `command.execute.before` | Before a slash command runs | Block or rewrite commands |
|
|
534
|
+
| `permission.ask` | Before a permission prompt | Auto-allow/deny |
|
|
535
|
+
| `shell.env` | Before `bash` runs | Inject env vars |
|
|
536
|
+
| `session.compact` | Before context compaction | React to compaction |
|
|
537
|
+
| `event` | Any published event | Generic subscription |
|
|
538
|
+
|
|
539
|
+
Handlers can be sync or `async`. They run sequentially so each can mutate the event before the next handler sees it. Raise `PermissionError` to block an action.
|
|
540
|
+
|
|
541
|
+
#### Loading plugins
|
|
542
|
+
|
|
543
|
+
Plugins come from three sources:
|
|
544
|
+
|
|
545
|
+
1. **Auto-discovery** — `.aru/plugins/*.py`, `.agents/plugins/*.py`, and the same paths under `~/`
|
|
546
|
+
2. **Config** — explicit list in `aru.json`:
|
|
547
|
+
|
|
548
|
+
```json
|
|
549
|
+
{
|
|
550
|
+
"plugins": [
|
|
551
|
+
"my-package-plugin",
|
|
552
|
+
["./.aru/plugins/audit.py", { "verbose": true }]
|
|
553
|
+
]
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
The second form passes options to the plugin as the `options` argument.
|
|
558
|
+
3. **Entry points** — installed packages can register via the `aru.plugins` entry point group
|
|
559
|
+
|
|
560
|
+
Every plugin file must export a `plugin(ctx, options)` function (sync or async) that returns a `Hooks` instance.
|
|
561
|
+
|
|
447
562
|
### MCP Support (Model Context Protocol)
|
|
448
563
|
|
|
449
564
|
Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
@@ -461,33 +576,53 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
|
461
576
|
|
|
462
577
|
## Agents
|
|
463
578
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
|
467
|
-
|
|
468
|
-
|
|
|
469
|
-
|
|
|
579
|
+
Built-in agents are declared as specs in `aru/agents/catalog.py` and instantiated on demand by `agent_factory.create_agent_from_spec`. A single construction path resolves the model, tool list, prompt role, and plugin hooks for all native agents.
|
|
580
|
+
|
|
581
|
+
| Agent | Mode | Role | Tools |
|
|
582
|
+
|-------|------|------|-------|
|
|
583
|
+
| **`build`** (General) | primary | Conversational coding assistant. Self-triggers `enter_plan_mode` for 3+ file changes | Full tool set including `delegate_task` |
|
|
584
|
+
| **`plan`** (Planner) | primary | Read-only analysis → `## Summary` + `## Steps` markdown plan | Read/search only (`read_file`, `read_files`, `glob_search`, `grep_search`, `list_directory`) |
|
|
585
|
+
| **`executor`** | primary | Step-by-step execution of a stored plan with mandatory task list tracking | Full tool set |
|
|
586
|
+
| **`explorer`** | **subagent** | Fast, read-only codebase research. Invoked only via `delegate_task(task, agent_name="explorer")` | Read/search + read-only `bash` + `rank_files` |
|
|
587
|
+
|
|
588
|
+
> **Scope reviewer:** `aru/agents/planner.py` also exposes `review_plan(request, plan)`, a one-shot, no-tool reviewer that runs on the small model to trim scope creep from generated plans. Enabled via `plan_reviewer: true` in `aru.json`.
|
|
589
|
+
|
|
590
|
+
### Plan mode flow
|
|
591
|
+
|
|
592
|
+
The `plan` agent runs in two ways:
|
|
593
|
+
|
|
594
|
+
1. **Manual:** the user types `/plan <task>` — the planner produces a plan, the reviewer optionally trims it, and the result is stored in the session.
|
|
595
|
+
2. **Autonomous:** the `build` agent calls `enter_plan_mode(task)` when it detects a multi-file task. This invokes the planner, stores the plan, and returns a summary.
|
|
596
|
+
|
|
597
|
+
Once a plan is stored, every following turn prepends a `<system-reminder>` listing all plan steps with their status icons. The build/executor agent works through them in order, calling `update_plan_step(index, "completed")` after each. Within a step, it calls `create_task_list([...])` to break the step into 1–10 concrete subtasks, then `update_task(i, "completed")` as they finish.
|
|
470
598
|
|
|
471
599
|
## Tools
|
|
472
600
|
|
|
473
601
|
### File Operations
|
|
474
602
|
- `read_file` — Reads files with line range support and binary detection
|
|
475
|
-
- `read_files` — Reads multiple files in parallel (
|
|
603
|
+
- `read_files` — Reads multiple files in parallel (batched)
|
|
476
604
|
- `write_file` — Writes content to files, creating directories as needed
|
|
605
|
+
- `write_files` — Writes multiple files in one call
|
|
477
606
|
- `edit_file` — Find-and-replace edits on files
|
|
607
|
+
- `edit_files` — Batched find-and-replace across multiple files
|
|
478
608
|
|
|
479
609
|
### Search & Discovery
|
|
480
610
|
- `glob_search` — Find files by pattern (respects .gitignore)
|
|
481
611
|
- `grep_search` — Content search with regex and file filtering
|
|
482
612
|
- `list_directory` — Directory listing with gitignore filtering
|
|
613
|
+
- `rank_files` — Multi-factor file relevance ranking (explorer subagent only)
|
|
483
614
|
|
|
484
615
|
### Shell & Web
|
|
485
616
|
- `bash` — Executes shell commands with permission gates
|
|
486
617
|
- `web_search` — Web search via DuckDuckGo
|
|
487
618
|
- `web_fetch` — Fetches URLs and converts HTML to readable text
|
|
488
619
|
|
|
489
|
-
###
|
|
490
|
-
- `
|
|
620
|
+
### Planning & Delegation
|
|
621
|
+
- `enter_plan_mode` — Generate a structured plan via the planner agent and store it in the session
|
|
622
|
+
- `update_plan_step` — Mark a macro plan step as `in_progress` / `completed` / `failed` / `skipped`
|
|
623
|
+
- `create_task_list` — Declare 1–10 subtasks for the current step (mandatory first executor call)
|
|
624
|
+
- `update_task` — Mark a subtask as `in_progress` / `completed` / `failed`
|
|
625
|
+
- `delegate_task` — Spawn an autonomous subagent (defaults to `explorer`) for parallel research or execution
|
|
491
626
|
|
|
492
627
|
## Architecture
|
|
493
628
|
|
|
@@ -495,22 +630,25 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
|
495
630
|
aru-code/
|
|
496
631
|
├── aru/
|
|
497
632
|
│ ├── cli.py # Main REPL loop, argument parsing, and entry point
|
|
498
|
-
│ ├── agent_factory.py #
|
|
633
|
+
│ ├── agent_factory.py # Single factory — builds Agno Agents from catalog specs
|
|
499
634
|
│ ├── commands.py # Slash commands, help display, shell execution
|
|
500
635
|
│ ├── completers.py # Input completions, paste detection, @file mentions
|
|
501
636
|
│ ├── context.py # Token optimization (pruning, truncation, compaction)
|
|
502
637
|
│ ├── display.py # Terminal display (logo, status bar, streaming output)
|
|
503
|
-
│ ├── runner.py # Agent execution
|
|
504
|
-
│ ├── session.py # Session state, persistence, plan tracking
|
|
638
|
+
│ ├── runner.py # Agent execution, streaming, PLAN ACTIVE reminder injection
|
|
639
|
+
│ ├── session.py # Session state, persistence, plan steps tracking
|
|
640
|
+
│ ├── runtime.py # Request context (TaskStore, session, display handles)
|
|
505
641
|
│ ├── config.py # Configuration loader (AGENTS.md, .agents/)
|
|
506
642
|
│ ├── providers.py # Multi-provider LLM abstraction
|
|
507
643
|
│ ├── permissions.py # Granular permission system (allow/ask/deny)
|
|
508
644
|
│ ├── agents/
|
|
509
|
-
│ │ ├──
|
|
510
|
-
│ │ ├──
|
|
511
|
-
│ │ └──
|
|
645
|
+
│ │ ├── base.py # Shared prompt templates + build_instructions(role)
|
|
646
|
+
│ │ ├── catalog.py # AgentSpec registry — build / plan / executor / explorer
|
|
647
|
+
│ │ └── planner.py # review_plan() — small-model scope reviewer
|
|
512
648
|
│ └── tools/
|
|
513
|
-
│ ├── codebase.py #
|
|
649
|
+
│ ├── codebase.py # Core tool implementations + GENERAL/EXECUTOR/PLANNER/EXPLORER sets
|
|
650
|
+
│ ├── plan_mode.py # enter_plan_mode tool (agent-invokable planner entry)
|
|
651
|
+
│ ├── tasklist.py # create_task_list / update_task / update_plan_step
|
|
514
652
|
│ ├── ast_tools.py # Tree-sitter code analysis
|
|
515
653
|
│ ├── ranker.py # File relevance ranking
|
|
516
654
|
│ ├── mcp_client.py # MCP client
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.26.0"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Agent creation:
|
|
1
|
+
"""Agent creation: catalog-driven factory plus custom agent instantiation."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -6,13 +6,40 @@ import functools
|
|
|
6
6
|
import inspect
|
|
7
7
|
import logging
|
|
8
8
|
|
|
9
|
+
from agno.compression.manager import CompressionManager
|
|
10
|
+
from agno.utils.log import log_warning
|
|
11
|
+
|
|
9
12
|
from aru.agents.base import build_instructions as _build_instructions
|
|
13
|
+
from aru.agents.catalog import AGENTS, AgentSpec
|
|
10
14
|
from aru.config import AgentConfig, CustomAgent
|
|
11
15
|
from aru.providers import create_model
|
|
12
16
|
from aru.session import Session
|
|
13
17
|
|
|
14
18
|
logger = logging.getLogger("aru.agent_factory")
|
|
15
19
|
|
|
20
|
+
# Max chars for truncation fallback when compression fails
|
|
21
|
+
_TRUNCATE_FALLBACK = 3000
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _SafeCompressionManager(CompressionManager):
|
|
25
|
+
"""CompressionManager that truncates on failure instead of leaving messages uncompressed.
|
|
26
|
+
|
|
27
|
+
Agno's default behavior: if compression returns None, the message stays with
|
|
28
|
+
compressed_content=None → should_compress() fires again → infinite retry loop.
|
|
29
|
+
This subclass marks failed messages with a truncated version so the loop moves on.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
async def acompress(self, messages, run_metrics=None):
|
|
33
|
+
before = {id(m) for m in messages if m.role == "tool" and m.compressed_content is None}
|
|
34
|
+
await super().acompress(messages, run_metrics=run_metrics)
|
|
35
|
+
for msg in messages:
|
|
36
|
+
if id(msg) in before and msg.compressed_content is None:
|
|
37
|
+
content_str = str(msg.content or "")
|
|
38
|
+
msg.compressed_content = content_str[:_TRUNCATE_FALLBACK] + (
|
|
39
|
+
"... [truncated, compression failed]" if len(content_str) > _TRUNCATE_FALLBACK else ""
|
|
40
|
+
)
|
|
41
|
+
log_warning(f"Compression fallback (truncate) for {msg.tool_name}")
|
|
42
|
+
|
|
16
43
|
|
|
17
44
|
def _wrap_tools_with_hooks(tools: list) -> list:
|
|
18
45
|
"""Wrap tool functions to fire tool.execute.before/after plugin hooks.
|
|
@@ -148,44 +175,77 @@ def _apply_chat_hooks(instructions: str, model_ref: str, agent_name: str,
|
|
|
148
175
|
return instructions, model_ref, max_tokens
|
|
149
176
|
|
|
150
177
|
|
|
151
|
-
def
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
178
|
+
def _make_compression_manager() -> _SafeCompressionManager:
|
|
179
|
+
"""Construct the safe compression manager used for every native agent."""
|
|
180
|
+
from aru.runtime import get_ctx
|
|
181
|
+
return _SafeCompressionManager(
|
|
182
|
+
model=create_model(get_ctx().small_model_ref, max_tokens=2048),
|
|
183
|
+
compress_tool_results=True,
|
|
184
|
+
compress_tool_results_limit=25,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def create_agent_from_spec(
|
|
189
|
+
spec: AgentSpec,
|
|
190
|
+
session: Session | None = None,
|
|
191
|
+
model_ref: str | None = None,
|
|
192
|
+
extra_instructions: str = "",
|
|
156
193
|
):
|
|
157
|
-
"""
|
|
194
|
+
"""Build an Agno Agent from a catalog spec.
|
|
158
195
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
196
|
+
Single construction path for all native agents (build/plan/executor/explorer).
|
|
197
|
+
Resolves model, wraps tools with plugin hooks, applies chat.system.transform
|
|
198
|
+
and chat.params hooks, and attaches the safe compression manager.
|
|
199
|
+
|
|
200
|
+
`session` may be None for subagent specs that always use the small model.
|
|
162
201
|
"""
|
|
163
202
|
from agno.agent import Agent
|
|
203
|
+
from aru.runtime import get_ctx
|
|
164
204
|
|
|
165
|
-
|
|
166
|
-
|
|
205
|
+
if spec.small_model:
|
|
206
|
+
resolved_model = model_ref or get_ctx().small_model_ref
|
|
207
|
+
else:
|
|
208
|
+
if session is None:
|
|
209
|
+
raise ValueError(f"AgentSpec {spec.name!r} requires a session to resolve the model")
|
|
210
|
+
resolved_model = model_ref or session.model_ref
|
|
167
211
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
extra = f"{extra}\n\n{env_context}" if extra else env_context
|
|
171
|
-
model_ref = model_override or session.model_ref
|
|
172
|
-
instructions = _build_instructions("general", extra)
|
|
212
|
+
tools = _wrap_tools_with_hooks(spec.tools_factory())
|
|
213
|
+
instructions = _build_instructions(spec.role, extra_instructions)
|
|
173
214
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
instructions, model_ref, "Aru", max_tokens=8192,
|
|
215
|
+
instructions, resolved_model, max_tokens = _apply_chat_hooks(
|
|
216
|
+
instructions, resolved_model, spec.name, max_tokens=spec.max_tokens,
|
|
177
217
|
)
|
|
178
218
|
|
|
179
219
|
return Agent(
|
|
180
|
-
name=
|
|
181
|
-
model=create_model(
|
|
220
|
+
name=spec.name,
|
|
221
|
+
model=create_model(resolved_model, max_tokens=max_tokens),
|
|
182
222
|
tools=tools,
|
|
183
223
|
instructions=instructions,
|
|
184
224
|
markdown=True,
|
|
225
|
+
compress_tool_results=True,
|
|
226
|
+
compression_manager=_make_compression_manager(),
|
|
185
227
|
tool_call_limit=None,
|
|
186
228
|
)
|
|
187
229
|
|
|
188
230
|
|
|
231
|
+
def create_general_agent(
|
|
232
|
+
session: Session,
|
|
233
|
+
config: AgentConfig | None = None,
|
|
234
|
+
model_override: str | None = None,
|
|
235
|
+
env_context: str = "",
|
|
236
|
+
):
|
|
237
|
+
"""Create the general-purpose agent (thin wrapper around the catalog factory)."""
|
|
238
|
+
extra = config.get_extra_instructions() if config else ""
|
|
239
|
+
if env_context:
|
|
240
|
+
extra = f"{extra}\n\n{env_context}" if extra else env_context
|
|
241
|
+
return create_agent_from_spec(
|
|
242
|
+
AGENTS["build"],
|
|
243
|
+
session,
|
|
244
|
+
model_ref=model_override or session.model_ref,
|
|
245
|
+
extra_instructions=extra,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
189
249
|
def create_custom_agent_instance(agent_def: CustomAgent, session: Session,
|
|
190
250
|
config: AgentConfig | None = None,
|
|
191
251
|
env_context: str = ""):
|