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.
Files changed (72) hide show
  1. {aru_code-0.25.2/aru_code.egg-info → aru_code-0.26.0}/PKG-INFO +158 -20
  2. {aru_code-0.25.2 → aru_code-0.26.0}/README.md +157 -19
  3. aru_code-0.26.0/aru/__init__.py +1 -0
  4. {aru_code-0.25.2 → aru_code-0.26.0}/aru/agent_factory.py +82 -22
  5. {aru_code-0.25.2 → aru_code-0.26.0}/aru/agents/base.py +67 -2
  6. aru_code-0.26.0/aru/agents/catalog.py +84 -0
  7. {aru_code-0.25.2 → aru_code-0.26.0}/aru/agents/planner.py +1 -40
  8. {aru_code-0.25.2 → aru_code-0.26.0}/aru/cli.py +15 -29
  9. {aru_code-0.25.2 → aru_code-0.26.0}/aru/runner.py +124 -292
  10. {aru_code-0.25.2 → aru_code-0.26.0}/aru/session.py +4 -0
  11. {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/codebase.py +51 -6
  12. aru_code-0.26.0/aru/tools/plan_mode.py +65 -0
  13. {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/tasklist.py +74 -0
  14. {aru_code-0.25.2 → aru_code-0.26.0/aru_code.egg-info}/PKG-INFO +158 -20
  15. {aru_code-0.25.2 → aru_code-0.26.0}/aru_code.egg-info/SOURCES.txt +3 -4
  16. {aru_code-0.25.2 → aru_code-0.26.0}/pyproject.toml +9 -1
  17. aru_code-0.26.0/tests/test_catalog.py +95 -0
  18. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_guardrails_scenarios.py +6 -0
  19. aru_code-0.25.2/aru/__init__.py +0 -1
  20. aru_code-0.25.2/aru/agents/executor.py +0 -59
  21. aru_code-0.25.2/aru/agents/explorer.py +0 -93
  22. aru_code-0.25.2/tests/test_executor.py +0 -81
  23. aru_code-0.25.2/tests/test_planner.py +0 -107
  24. {aru_code-0.25.2 → aru_code-0.26.0}/LICENSE +0 -0
  25. {aru_code-0.25.2 → aru_code-0.26.0}/aru/agents/__init__.py +0 -0
  26. {aru_code-0.25.2 → aru_code-0.26.0}/aru/cache_patch.py +0 -0
  27. {aru_code-0.25.2 → aru_code-0.26.0}/aru/checkpoints.py +0 -0
  28. {aru_code-0.25.2 → aru_code-0.26.0}/aru/commands.py +0 -0
  29. {aru_code-0.25.2 → aru_code-0.26.0}/aru/completers.py +0 -0
  30. {aru_code-0.25.2 → aru_code-0.26.0}/aru/config.py +0 -0
  31. {aru_code-0.25.2 → aru_code-0.26.0}/aru/context.py +0 -0
  32. {aru_code-0.25.2 → aru_code-0.26.0}/aru/display.py +0 -0
  33. {aru_code-0.25.2 → aru_code-0.26.0}/aru/history_blocks.py +0 -0
  34. {aru_code-0.25.2 → aru_code-0.26.0}/aru/permissions.py +0 -0
  35. {aru_code-0.25.2 → aru_code-0.26.0}/aru/plugins/__init__.py +0 -0
  36. {aru_code-0.25.2 → aru_code-0.26.0}/aru/plugins/custom_tools.py +0 -0
  37. {aru_code-0.25.2 → aru_code-0.26.0}/aru/plugins/hooks.py +0 -0
  38. {aru_code-0.25.2 → aru_code-0.26.0}/aru/plugins/manager.py +0 -0
  39. {aru_code-0.25.2 → aru_code-0.26.0}/aru/plugins/tool_api.py +0 -0
  40. {aru_code-0.25.2 → aru_code-0.26.0}/aru/providers.py +0 -0
  41. {aru_code-0.25.2 → aru_code-0.26.0}/aru/runtime.py +0 -0
  42. {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/__init__.py +0 -0
  43. {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/ast_tools.py +0 -0
  44. {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/gitignore.py +0 -0
  45. {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/mcp_client.py +0 -0
  46. {aru_code-0.25.2 → aru_code-0.26.0}/aru/tools/ranker.py +0 -0
  47. {aru_code-0.25.2 → aru_code-0.26.0}/aru_code.egg-info/dependency_links.txt +0 -0
  48. {aru_code-0.25.2 → aru_code-0.26.0}/aru_code.egg-info/entry_points.txt +0 -0
  49. {aru_code-0.25.2 → aru_code-0.26.0}/aru_code.egg-info/requires.txt +0 -0
  50. {aru_code-0.25.2 → aru_code-0.26.0}/aru_code.egg-info/top_level.txt +0 -0
  51. {aru_code-0.25.2 → aru_code-0.26.0}/setup.cfg +0 -0
  52. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_agents_base.py +0 -0
  53. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_checkpoints.py +0 -0
  54. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli.py +0 -0
  55. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_advanced.py +0 -0
  56. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_base.py +0 -0
  57. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_completers.py +0 -0
  58. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_new.py +0 -0
  59. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_run_cli.py +0 -0
  60. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_session.py +0 -0
  61. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_cli_shell.py +0 -0
  62. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_codebase.py +0 -0
  63. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_confabulation_regression.py +0 -0
  64. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_config.py +0 -0
  65. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_context.py +0 -0
  66. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_gitignore.py +0 -0
  67. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_main.py +0 -0
  68. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_mcp_client.py +0 -0
  69. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_permissions.py +0 -0
  70. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_plugins.py +0 -0
  71. {aru_code-0.25.2 → aru_code-0.26.0}/tests/test_providers.py +0 -0
  72. {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.25.2
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
  ![0329(3)](https://github.com/user-attachments/assets/e84d5139-ebaa-4d12-bbae-628fae7dbc7a)
53
55
 
54
56
  ## Highlights
55
57
 
56
- - **Multi-Agent Architecture** — Specialized agents for planning, execution, exploration, and conversation
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
- - **11 Integrated Tools** — File operations, code search, shell, web search, task delegation
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
- | Agent | Role | Tools |
512
- |-------|------|-------|
513
- | **Planner** | Analyzes codebase, creates structured implementation plans | Read-only tools, search, web |
514
- | **Executor** | Implements code changes based on plans or instructions | All tools including delegation |
515
- | **General** | Handles conversation and simple operations | All tools including delegation |
516
- | **Explorer** | Fast, read-only codebase exploration and search | Read-only tools, search, bash (read-only) |
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 (single batched call)
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
- ### Advanced
537
- - `delegate_task` — Spawns autonomous sub-agents for parallel task execution
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 # Agent instantiation (general and custom agents)
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 orchestration with streaming
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
- │ │ ├── planner.py # Planning agent
557
- │ │ ├── executor.py # Execution agent
558
- │ │ └── explorer.py # Explorer agent (fast, read-only codebase search)
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 # 11 core tools
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
  ![0329(3)](https://github.com/user-attachments/assets/e84d5139-ebaa-4d12-bbae-628fae7dbc7a)
6
8
 
7
9
  ## Highlights
8
10
 
9
- - **Multi-Agent Architecture** — Specialized agents for planning, execution, exploration, and conversation
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
- - **11 Integrated Tools** — File operations, code search, shell, web search, task delegation
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
- | Agent | Role | Tools |
465
- |-------|------|-------|
466
- | **Planner** | Analyzes codebase, creates structured implementation plans | Read-only tools, search, web |
467
- | **Executor** | Implements code changes based on plans or instructions | All tools including delegation |
468
- | **General** | Handles conversation and simple operations | All tools including delegation |
469
- | **Explorer** | Fast, read-only codebase exploration and search | Read-only tools, search, bash (read-only) |
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 (single batched call)
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
- ### Advanced
490
- - `delegate_task` — Spawns autonomous sub-agents for parallel task execution
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 # Agent instantiation (general and custom agents)
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 orchestration with streaming
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
- │ │ ├── planner.py # Planning agent
510
- │ │ ├── executor.py # Execution agent
511
- │ │ └── explorer.py # Explorer agent (fast, read-only codebase search)
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 # 11 core tools
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: general-purpose and custom agent instantiation."""
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 create_general_agent(
152
- session: Session,
153
- config: AgentConfig | None = None,
154
- model_override: str | None = None,
155
- env_context: str = "",
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
- """Create the general-purpose agent.
194
+ """Build an Agno Agent from a catalog spec.
158
195
 
159
- Args:
160
- env_context: Environment context (cwd, tree, git status) to include
161
- in the system prompt. Placed in instructions so it's cacheable.
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
- from aru.tools.codebase import GENERAL_TOOLS
166
- tools = _wrap_tools_with_hooks(GENERAL_TOOLS)
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
- extra = config.get_extra_instructions() if config else ""
169
- if env_context:
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
- # Apply chat hooks (system.transform + params)
175
- instructions, model_ref, max_tokens = _apply_chat_hooks(
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="Aru",
181
- model=create_model(model_ref, max_tokens=max_tokens),
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 = ""):