aru-code 0.22.1__tar.gz → 0.24.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.22.1/aru_code.egg-info → aru_code-0.24.0}/PKG-INFO +1 -1
- aru_code-0.24.0/aru/__init__.py +1 -0
- aru_code-0.24.0/aru/agent_factory.py +220 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/agents/base.py +32 -2
- aru_code-0.24.0/aru/agents/explorer.py +91 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/cache_patch.py +3 -3
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/cli.py +49 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/config.py +1 -1
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/context.py +17 -25
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/display.py +18 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/plugins/__init__.py +2 -1
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/plugins/hooks.py +73 -3
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/plugins/manager.py +63 -1
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/runner.py +75 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/tools/codebase.py +39 -32
- {aru_code-0.22.1 → aru_code-0.24.0/aru_code.egg-info}/PKG-INFO +1 -1
- {aru_code-0.22.1 → aru_code-0.24.0}/aru_code.egg-info/SOURCES.txt +2 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/pyproject.toml +1 -1
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_codebase.py +1 -1
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_context.py +5 -5
- aru_code-0.24.0/tests/test_guardrails_scenarios.py +199 -0
- aru_code-0.22.1/aru/__init__.py +0 -1
- aru_code-0.22.1/aru/agent_factory.py +0 -69
- {aru_code-0.22.1 → aru_code-0.24.0}/LICENSE +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/README.md +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/agents/executor.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/agents/planner.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/checkpoints.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/commands.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/completers.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/history_blocks.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/permissions.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/plugins/custom_tools.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/plugins/tool_api.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/providers.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/runtime.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/session.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/setup.cfg +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_checkpoints.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_cli.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_confabulation_regression.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_config.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_executor.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_main.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_permissions.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_planner.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_plugins.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_providers.py +0 -0
- {aru_code-0.22.1 → aru_code-0.24.0}/tests/test_ranker.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.24.0"
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Agent creation: general-purpose and custom agent instantiation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from aru.agents.base import build_instructions as _build_instructions
|
|
10
|
+
from aru.config import AgentConfig, CustomAgent
|
|
11
|
+
from aru.providers import create_model
|
|
12
|
+
from aru.session import Session
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("aru.agent_factory")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _wrap_tools_with_hooks(tools: list) -> list:
|
|
18
|
+
"""Wrap tool functions to fire tool.execute.before/after plugin hooks.
|
|
19
|
+
|
|
20
|
+
Before hook can mutate args; after hook can mutate the result.
|
|
21
|
+
If a before hook raises, the tool is not executed and the error is returned.
|
|
22
|
+
"""
|
|
23
|
+
from aru.runtime import get_ctx
|
|
24
|
+
|
|
25
|
+
async def _fire(event_name: str, data: dict) -> dict:
|
|
26
|
+
try:
|
|
27
|
+
ctx = get_ctx()
|
|
28
|
+
mgr = ctx.plugin_manager
|
|
29
|
+
if mgr is not None and mgr.loaded:
|
|
30
|
+
event = await mgr.fire(event_name, data)
|
|
31
|
+
return event.data
|
|
32
|
+
except (LookupError, AttributeError):
|
|
33
|
+
pass
|
|
34
|
+
return data
|
|
35
|
+
|
|
36
|
+
async def _fire_tool_definition(tool_name: str, description: str, parameters: dict) -> dict:
|
|
37
|
+
"""Fire tool.definition hook — plugins can modify tool desc/params."""
|
|
38
|
+
try:
|
|
39
|
+
ctx = get_ctx()
|
|
40
|
+
mgr = ctx.plugin_manager
|
|
41
|
+
if mgr is not None and mgr.loaded:
|
|
42
|
+
event = await mgr.fire("tool.definition", {
|
|
43
|
+
"tool_name": tool_name,
|
|
44
|
+
"description": description,
|
|
45
|
+
"parameters": parameters,
|
|
46
|
+
})
|
|
47
|
+
return event.data
|
|
48
|
+
except (LookupError, AttributeError):
|
|
49
|
+
pass
|
|
50
|
+
return {"tool_name": tool_name, "description": description, "parameters": parameters}
|
|
51
|
+
|
|
52
|
+
def _wrap_one(fn):
|
|
53
|
+
if not callable(fn) or getattr(fn, "_hook_wrapped", False):
|
|
54
|
+
return fn
|
|
55
|
+
|
|
56
|
+
@functools.wraps(fn)
|
|
57
|
+
async def wrapper(**kwargs):
|
|
58
|
+
tool_name = fn.__name__
|
|
59
|
+
# Before hook — plugins can mutate args or raise PermissionError to block
|
|
60
|
+
try:
|
|
61
|
+
before_data = await _fire("tool.execute.before", {
|
|
62
|
+
"tool_name": tool_name,
|
|
63
|
+
"args": kwargs,
|
|
64
|
+
})
|
|
65
|
+
kwargs = before_data.get("args", kwargs)
|
|
66
|
+
except PermissionError as e:
|
|
67
|
+
return f"BLOCKED by plugin: {e}. Do NOT retry this operation."
|
|
68
|
+
|
|
69
|
+
# Execute the tool
|
|
70
|
+
if inspect.iscoroutinefunction(fn):
|
|
71
|
+
result = await fn(**kwargs)
|
|
72
|
+
else:
|
|
73
|
+
result = fn(**kwargs)
|
|
74
|
+
|
|
75
|
+
# After hook — plugins can mutate the result
|
|
76
|
+
after_data = await _fire("tool.execute.after", {
|
|
77
|
+
"tool_name": tool_name,
|
|
78
|
+
"args": kwargs,
|
|
79
|
+
"result": result,
|
|
80
|
+
})
|
|
81
|
+
return after_data.get("result", result)
|
|
82
|
+
|
|
83
|
+
wrapper._hook_wrapped = True
|
|
84
|
+
return wrapper
|
|
85
|
+
|
|
86
|
+
return [_wrap_one(t) for t in tools]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _fire_sync_hook(event_name: str, data: dict) -> dict:
|
|
90
|
+
"""Fire a plugin hook synchronously (for agent creation context).
|
|
91
|
+
|
|
92
|
+
Agent creation happens in sync code, so we need a sync path.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
from aru.runtime import get_ctx
|
|
96
|
+
ctx = get_ctx()
|
|
97
|
+
mgr = ctx.plugin_manager
|
|
98
|
+
if mgr is not None and mgr.loaded:
|
|
99
|
+
import asyncio
|
|
100
|
+
from aru.plugins.hooks import HookEvent
|
|
101
|
+
event = HookEvent(hook=event_name, data=data or {})
|
|
102
|
+
for hooks in mgr._hooks:
|
|
103
|
+
for handler in hooks.get_handlers(event_name):
|
|
104
|
+
try:
|
|
105
|
+
if asyncio.iscoroutinefunction(handler):
|
|
106
|
+
# Best-effort: try to run async handler
|
|
107
|
+
try:
|
|
108
|
+
loop = asyncio.get_running_loop()
|
|
109
|
+
except RuntimeError:
|
|
110
|
+
loop = None
|
|
111
|
+
if loop and loop.is_running():
|
|
112
|
+
# Can't await in sync context with running loop — skip
|
|
113
|
+
continue
|
|
114
|
+
else:
|
|
115
|
+
asyncio.run(handler(event))
|
|
116
|
+
else:
|
|
117
|
+
handler(event)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.warning("Hook handler error (%s): %s", event_name, e)
|
|
120
|
+
return event.data
|
|
121
|
+
except (LookupError, AttributeError):
|
|
122
|
+
pass
|
|
123
|
+
return data
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _apply_chat_hooks(instructions: str, model_ref: str, agent_name: str,
|
|
127
|
+
max_tokens: int = 8192) -> tuple[str, str, int]:
|
|
128
|
+
"""Apply chat.system.transform and chat.params hooks to agent creation params.
|
|
129
|
+
|
|
130
|
+
Returns (instructions, model_ref, max_tokens) — possibly modified by plugins.
|
|
131
|
+
"""
|
|
132
|
+
# chat.system.transform — plugins can modify the system prompt
|
|
133
|
+
data = _fire_sync_hook("chat.system.transform", {
|
|
134
|
+
"system_prompt": instructions,
|
|
135
|
+
"agent": agent_name,
|
|
136
|
+
})
|
|
137
|
+
instructions = data.get("system_prompt", instructions)
|
|
138
|
+
|
|
139
|
+
# chat.params — plugins can modify LLM parameters
|
|
140
|
+
data = _fire_sync_hook("chat.params", {
|
|
141
|
+
"model": model_ref,
|
|
142
|
+
"max_tokens": max_tokens,
|
|
143
|
+
"temperature": None, # let plugin set if desired
|
|
144
|
+
})
|
|
145
|
+
model_ref = data.get("model", model_ref)
|
|
146
|
+
max_tokens = data.get("max_tokens", max_tokens)
|
|
147
|
+
|
|
148
|
+
return instructions, model_ref, max_tokens
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def create_general_agent(
|
|
152
|
+
session: Session,
|
|
153
|
+
config: AgentConfig | None = None,
|
|
154
|
+
model_override: str | None = None,
|
|
155
|
+
env_context: str = "",
|
|
156
|
+
):
|
|
157
|
+
"""Create the general-purpose agent.
|
|
158
|
+
|
|
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.
|
|
162
|
+
"""
|
|
163
|
+
from agno.agent import Agent
|
|
164
|
+
|
|
165
|
+
from aru.tools.codebase import GENERAL_TOOLS
|
|
166
|
+
tools = _wrap_tools_with_hooks(GENERAL_TOOLS)
|
|
167
|
+
|
|
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)
|
|
173
|
+
|
|
174
|
+
# Apply chat hooks (system.transform + params)
|
|
175
|
+
instructions, model_ref, max_tokens = _apply_chat_hooks(
|
|
176
|
+
instructions, model_ref, "Aru", max_tokens=8192,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return Agent(
|
|
180
|
+
name="Aru",
|
|
181
|
+
model=create_model(model_ref, max_tokens=max_tokens),
|
|
182
|
+
tools=tools,
|
|
183
|
+
instructions=instructions,
|
|
184
|
+
markdown=True,
|
|
185
|
+
tool_call_limit=20,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def create_custom_agent_instance(agent_def: CustomAgent, session: Session,
|
|
190
|
+
config: AgentConfig | None = None,
|
|
191
|
+
env_context: str = ""):
|
|
192
|
+
"""Create an Agno Agent from a CustomAgent definition."""
|
|
193
|
+
from agno.agent import Agent
|
|
194
|
+
from aru.agents.base import BASE_INSTRUCTIONS
|
|
195
|
+
from aru.tools.codebase import resolve_tools
|
|
196
|
+
|
|
197
|
+
model_ref = agent_def.model or session.model_ref
|
|
198
|
+
tools = _wrap_tools_with_hooks(resolve_tools(agent_def.tools))
|
|
199
|
+
|
|
200
|
+
extra = config.get_extra_instructions() if config else ""
|
|
201
|
+
if env_context:
|
|
202
|
+
extra = f"{extra}\n\n{env_context}" if extra else env_context
|
|
203
|
+
parts = [agent_def.system_prompt, BASE_INSTRUCTIONS]
|
|
204
|
+
if extra:
|
|
205
|
+
parts.append(extra)
|
|
206
|
+
instructions = "\n\n".join(parts)
|
|
207
|
+
|
|
208
|
+
# Apply chat hooks (system.transform + params)
|
|
209
|
+
instructions, model_ref, max_tokens = _apply_chat_hooks(
|
|
210
|
+
instructions, model_ref, agent_def.name, max_tokens=8192,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return Agent(
|
|
214
|
+
name=agent_def.name,
|
|
215
|
+
model=create_model(model_ref, max_tokens=max_tokens),
|
|
216
|
+
tools=tools,
|
|
217
|
+
instructions=instructions,
|
|
218
|
+
markdown=True,
|
|
219
|
+
tool_call_limit=agent_def.max_turns or 20,
|
|
220
|
+
)
|
|
@@ -161,7 +161,10 @@ Use `context_lines=30` for full function bodies.
|
|
|
161
161
|
|
|
162
162
|
**Batch independent tool calls**: emit ALL independent tool calls in a single response.
|
|
163
163
|
|
|
164
|
-
Use delegate_task to split work into independent subtasks for parallel execution.
|
|
164
|
+
Use delegate_task to split work into independent subtasks for parallel execution. \
|
|
165
|
+
For broad codebase exploration (searching many files, finding patterns, understanding code), \
|
|
166
|
+
break the research into focused questions and spawn multiple \
|
|
167
|
+
`delegate_task(task="<specific search>", agent_name="explorer")` calls in parallel.
|
|
165
168
|
|
|
166
169
|
When given a plan, execute it step by step. When given a direct task, figure out what needs to be done and do it.
|
|
167
170
|
**ZERO narration between tool calls.** No "Now I have enough context...", \
|
|
@@ -208,7 +211,34 @@ Batch what you need upfront, then execute.
|
|
|
208
211
|
|
|
209
212
|
**When adding or modifying unit tests, ALWAYS run them to verify they pass before finishing.**
|
|
210
213
|
|
|
211
|
-
|
|
214
|
+
## Delegation strategy — CRITICAL for context efficiency
|
|
215
|
+
|
|
216
|
+
For simple, directed lookups (one known file, one specific symbol) use \
|
|
217
|
+
`grep_search` / `glob_search` / `read_file` directly.
|
|
218
|
+
|
|
219
|
+
For **anything broader** — understanding a system, researching before implementing, \
|
|
220
|
+
analyzing multiple files, writing specs or documentation — **always use explorer agents**. \
|
|
221
|
+
Every `read_file` / `read_file_smart` / `grep_search` result you call directly accumulates \
|
|
222
|
+
in YOUR context window and stays there forever. Explorer agents read files in their own \
|
|
223
|
+
isolated context and return only a concise summary. This is critical: \
|
|
224
|
+
**3 explorer summaries < 8 raw file reads** in context cost.
|
|
225
|
+
|
|
226
|
+
**Rule of thumb**: If you'd need to read or search more than 2-3 files, use explorers instead.
|
|
227
|
+
|
|
228
|
+
**Decompose, don't dump.** Never throw one vague task at one explorer. \
|
|
229
|
+
Break the work into **focused, independent search questions** and spawn one explorer \
|
|
230
|
+
per question — all in a single response so they run in parallel. Each explorer prompt \
|
|
231
|
+
should be specific enough that it can search and answer on its own.
|
|
232
|
+
|
|
233
|
+
Example — user asks "explain the authentication system":
|
|
234
|
+
```
|
|
235
|
+
delegate_task(task="Find auth middleware: search for login/logout handlers, session management, token validation", agent_name="explorer")
|
|
236
|
+
delegate_task(task="Find auth configuration: search for auth-related config files, env vars, secrets setup", agent_name="explorer")
|
|
237
|
+
delegate_task(task="Find auth tests: search for test files covering authentication flows", agent_name="explorer")
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
After all explorers return, **synthesize their findings yourself** — the user sees \
|
|
241
|
+
your summary, not the raw explorer output.\
|
|
212
242
|
"""
|
|
213
243
|
|
|
214
244
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Explorer agent — fast, read-only codebase exploration specialist."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from agno.agent import Agent
|
|
6
|
+
|
|
7
|
+
from aru.providers import create_model
|
|
8
|
+
from aru.runtime import get_ctx
|
|
9
|
+
from aru.tools.codebase import (
|
|
10
|
+
bash,
|
|
11
|
+
glob_search,
|
|
12
|
+
grep_search,
|
|
13
|
+
list_directory,
|
|
14
|
+
rank_files,
|
|
15
|
+
read_file,
|
|
16
|
+
read_file_smart,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Read-only tools only — no write/edit/delegate (prevents recursion and mutations)
|
|
20
|
+
EXPLORER_TOOLS = [
|
|
21
|
+
read_file,
|
|
22
|
+
read_file_smart,
|
|
23
|
+
glob_search,
|
|
24
|
+
grep_search,
|
|
25
|
+
list_directory,
|
|
26
|
+
bash,
|
|
27
|
+
rank_files,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
EXPLORER_ROLE = """\
|
|
31
|
+
You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
|
32
|
+
|
|
33
|
+
=== CRITICAL: READ-ONLY MODE — NO FILE MODIFICATIONS ===
|
|
34
|
+
This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:
|
|
35
|
+
- Creating new files (no write_file, touch, or file creation of any kind)
|
|
36
|
+
- Modifying existing files (no edit_file operations)
|
|
37
|
+
- Deleting files (no rm or deletion)
|
|
38
|
+
- Moving or copying files (no mv or cp)
|
|
39
|
+
- Creating temporary files anywhere, including /tmp
|
|
40
|
+
- Using redirect operators (>, >>, |) or heredocs to write to files
|
|
41
|
+
- Running ANY commands that change system state
|
|
42
|
+
|
|
43
|
+
Your role is EXCLUSIVELY to search and analyze existing code. \
|
|
44
|
+
You do NOT have access to file editing tools — attempting to edit files will fail.
|
|
45
|
+
|
|
46
|
+
Your strengths:
|
|
47
|
+
- Rapidly finding files using glob patterns
|
|
48
|
+
- Searching code and text with powerful regex patterns
|
|
49
|
+
- Reading and analyzing file contents
|
|
50
|
+
|
|
51
|
+
Guidelines:
|
|
52
|
+
- Use glob_search for broad file pattern matching
|
|
53
|
+
- Use grep_search for searching file contents with regex
|
|
54
|
+
- Use read_file when you know the specific file path you need to read
|
|
55
|
+
- Use read_file_smart when you know the file and have a specific question about it
|
|
56
|
+
- Use bash ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)
|
|
57
|
+
- NEVER use bash for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, \
|
|
58
|
+
or any file creation/modification
|
|
59
|
+
- Adapt your search approach based on the thoroughness level specified by the caller
|
|
60
|
+
|
|
61
|
+
NOTE: You are meant to be a FAST agent that returns output as quickly as possible. To achieve this:
|
|
62
|
+
- Make efficient use of tools: be smart about how you search for files and implementations
|
|
63
|
+
- Wherever possible, spawn MULTIPLE PARALLEL tool calls for grepping and reading files
|
|
64
|
+
- Do not read files you don't need — stop as soon as you have enough information
|
|
65
|
+
|
|
66
|
+
Complete the search request efficiently and report your findings clearly.\
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def create_explorer(task: str, context: str = "") -> Agent:
|
|
71
|
+
"""Create and return an explorer agent for a specific task.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
task: The exploration task description.
|
|
75
|
+
context: Optional extra context (file paths, constraints).
|
|
76
|
+
"""
|
|
77
|
+
cwd = os.getcwd()
|
|
78
|
+
small_model_ref = get_ctx().small_model_ref
|
|
79
|
+
|
|
80
|
+
instructions = EXPLORER_ROLE + f"\nThe current working directory is: {cwd}\n"
|
|
81
|
+
if context:
|
|
82
|
+
instructions += f"\nAdditional context:\n{context}\n"
|
|
83
|
+
|
|
84
|
+
return Agent(
|
|
85
|
+
name="Explorer",
|
|
86
|
+
model=create_model(small_model_ref, max_tokens=4096),
|
|
87
|
+
tools=EXPLORER_TOOLS,
|
|
88
|
+
instructions=instructions,
|
|
89
|
+
markdown=True,
|
|
90
|
+
tool_call_limit=15,
|
|
91
|
+
)
|
|
@@ -22,9 +22,9 @@ from __future__ import annotations
|
|
|
22
22
|
# - Protect recent tool results within a token budget
|
|
23
23
|
# - Only prune if there's enough to free (avoid churn)
|
|
24
24
|
# - Walk backwards, protecting recent content first
|
|
25
|
-
#
|
|
26
|
-
_PRUNE_PROTECT_CHARS =
|
|
27
|
-
_PRUNE_MINIMUM_CHARS =
|
|
25
|
+
# OpenCode uses 40K protect / 20K minimum; we use chars (~4 chars/token)
|
|
26
|
+
_PRUNE_PROTECT_CHARS = 160_000 # ~40K tokens — recent content always kept
|
|
27
|
+
_PRUNE_MINIMUM_CHARS = 80_000 # ~20K tokens — only prune if this much is freeable
|
|
28
28
|
_PRUNED_PLACEHOLDER = "[Old tool result cleared]"
|
|
29
29
|
|
|
30
30
|
# Last API call metrics (updated on every internal API call)
|
|
@@ -228,10 +228,20 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
228
228
|
ctx.plugin_manager = _plugin_mgr
|
|
229
229
|
|
|
230
230
|
try:
|
|
231
|
+
_config_dict = {
|
|
232
|
+
"default_model": config.default_model,
|
|
233
|
+
"model_aliases": config.model_aliases,
|
|
234
|
+
"permissions": config.permissions,
|
|
235
|
+
"plugin_specs": config.plugin_specs,
|
|
236
|
+
"disabled_tools": config.disabled_tools,
|
|
237
|
+
"plan_reviewer": config.plan_reviewer,
|
|
238
|
+
}
|
|
231
239
|
_p_input = PluginInput(
|
|
232
240
|
directory=os.getcwd(),
|
|
233
241
|
config_path="aru.json" if os.path.isfile("aru.json") else "",
|
|
234
242
|
model_ref=session.model_ref,
|
|
243
|
+
config=_config_dict,
|
|
244
|
+
session=session,
|
|
235
245
|
)
|
|
236
246
|
_plugin_specs = config.plugin_specs if hasattr(config, "plugin_specs") else []
|
|
237
247
|
_plugin_count = await _plugin_mgr.load_all(_p_input, plugin_specs=_plugin_specs)
|
|
@@ -252,6 +262,17 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
252
262
|
|
|
253
263
|
asyncio.create_task(_load_mcp_background())
|
|
254
264
|
|
|
265
|
+
# Event: session.start
|
|
266
|
+
if _plugin_mgr.loaded:
|
|
267
|
+
try:
|
|
268
|
+
await _plugin_mgr.publish("session.start", {
|
|
269
|
+
"session_id": getattr(session, "id", None),
|
|
270
|
+
"model_ref": session.model_ref,
|
|
271
|
+
"directory": os.getcwd(),
|
|
272
|
+
})
|
|
273
|
+
except Exception:
|
|
274
|
+
pass
|
|
275
|
+
|
|
255
276
|
while True:
|
|
256
277
|
try:
|
|
257
278
|
paste_state.clear()
|
|
@@ -393,6 +414,14 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
393
414
|
continue
|
|
394
415
|
|
|
395
416
|
if user_input.lower() in ("/quit", "/exit", "quit", "exit"):
|
|
417
|
+
# Event: session.end
|
|
418
|
+
if _plugin_mgr.loaded:
|
|
419
|
+
try:
|
|
420
|
+
await _plugin_mgr.publish("session.end", {
|
|
421
|
+
"session_id": getattr(session, "id", None),
|
|
422
|
+
})
|
|
423
|
+
except Exception:
|
|
424
|
+
pass
|
|
396
425
|
store.save(session)
|
|
397
426
|
console.print(f"\n[dim]Session saved: {session.session_id}[/dim]")
|
|
398
427
|
console.print(f"[dim]Resume with:[/dim] [bold cyan]aru --resume {session.session_id}[/bold cyan]")
|
|
@@ -579,6 +608,26 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
579
608
|
cmd_name = parts[0].lower()
|
|
580
609
|
cmd_args = parts[1] if len(parts) > 1 else ""
|
|
581
610
|
|
|
611
|
+
# Hook: command.execute.before — plugins can block or modify
|
|
612
|
+
_cmd_blocked = False
|
|
613
|
+
try:
|
|
614
|
+
_mgr = ctx.plugin_manager
|
|
615
|
+
if _mgr is not None and _mgr.loaded:
|
|
616
|
+
_cmd_event = await _mgr.fire("command.execute.before", {
|
|
617
|
+
"command": cmd_name,
|
|
618
|
+
"command_args": cmd_args,
|
|
619
|
+
"blocked": False,
|
|
620
|
+
})
|
|
621
|
+
if _cmd_event.data.get("blocked"):
|
|
622
|
+
console.print(f"[yellow]Command /{cmd_name} blocked by plugin.[/yellow]")
|
|
623
|
+
_cmd_blocked = True
|
|
624
|
+
else:
|
|
625
|
+
cmd_args = _cmd_event.data.get("command_args", cmd_args)
|
|
626
|
+
except Exception:
|
|
627
|
+
pass
|
|
628
|
+
if _cmd_blocked:
|
|
629
|
+
continue
|
|
630
|
+
|
|
582
631
|
if cmd_name in config.commands:
|
|
583
632
|
cmd_def = config.commands[cmd_name]
|
|
584
633
|
prompt = render_command_template(cmd_def.template, cmd_args)
|
|
@@ -552,7 +552,7 @@ def load_config(cwd: str | None = None) -> AgentConfig:
|
|
|
552
552
|
# Load config: global (~/.aru/config.json) first, then project-level on top.
|
|
553
553
|
# Project values override global values via deep merge.
|
|
554
554
|
home = Path.home()
|
|
555
|
-
global_config_paths = [home / ".aru" / "config.json"]
|
|
555
|
+
global_config_paths = [home / ".aru" / "aru.json", home / ".aru" / "config.json"]
|
|
556
556
|
project_config_paths = [root / "aru.json", root / ".aru" / "config.json"]
|
|
557
557
|
|
|
558
558
|
merged_data: dict = {}
|
|
@@ -24,8 +24,8 @@ from __future__ import annotations
|
|
|
24
24
|
# ── Constants ──────────────────────────────────────────────────────
|
|
25
25
|
|
|
26
26
|
# Pruning: minimum chars that must be freeable to justify a prune pass.
|
|
27
|
-
#
|
|
28
|
-
PRUNE_MINIMUM_CHARS =
|
|
27
|
+
# Matches opencode's PRUNE_MINIMUM = 20_000 tokens (~80K chars @ 4 chars/token).
|
|
28
|
+
PRUNE_MINIMUM_CHARS = 80_000 # ~20K tokens
|
|
29
29
|
# Placeholder that replaces cleared tool_result content. Matches
|
|
30
30
|
# cache_patch.py's _PRUNED_PLACEHOLDER so both layers produce identical
|
|
31
31
|
# text when a tool output is cleared.
|
|
@@ -48,21 +48,9 @@ TRUNCATE_MAX_LINE_LENGTH = 1500 # chars per individual line (prevents minified
|
|
|
48
48
|
TRUNCATE_SAVE_DIR = ".aru/truncated"
|
|
49
49
|
|
|
50
50
|
# Compaction: chars of recent conversation preserved verbatim post-compact.
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
# - Prune protect: "how much tool_result content stays intact"
|
|
55
|
-
# - Compact recent: "how much full-message history stays verbatim after
|
|
56
|
-
# the summary replaces the older portion"
|
|
57
|
-
#
|
|
58
|
-
# Set to 80K chars (~20K tokens) — half the prune window. Rationale:
|
|
59
|
-
# with the compactor now running on the main model (not a small one),
|
|
60
|
-
# summaries are faithful enough that we don't need 40K of recent overlap
|
|
61
|
-
# as a safety net. 20K still covers 3-6 recent turns verbatim, which
|
|
62
|
-
# mirrors the "last few exchanges" a human would re-read to resume work.
|
|
63
|
-
# Going to zero would match opencode exactly but requires the reactive
|
|
64
|
-
# overflow replay flow we haven't implemented yet.
|
|
65
|
-
COMPACT_RECENT_CHARS = 80_000
|
|
51
|
+
# Uses the same budget as prune protect (160K chars ≈ 40K tokens) to match
|
|
52
|
+
# opencode's approach where the split point mirrors the prune window.
|
|
53
|
+
COMPACT_RECENT_CHARS = 160_000
|
|
66
54
|
|
|
67
55
|
# Compaction: trigger when per-call input tokens approach real overflow.
|
|
68
56
|
# Matches opencode's philosophy: only fire near the model's actual context
|
|
@@ -177,20 +165,24 @@ def _tool_result_content_len(msg: dict) -> int:
|
|
|
177
165
|
|
|
178
166
|
|
|
179
167
|
def _get_prune_protect_chars(model_id: str = "default") -> int:
|
|
180
|
-
"""Chars of recent
|
|
168
|
+
"""Chars of recent history that must NEVER be pruned.
|
|
169
|
+
|
|
170
|
+
Flat value across all models, mirroring opencode's fixed
|
|
171
|
+
`PRUNE_PROTECT = 40_000` tokens (compaction.ts:36). At ~4 chars/token
|
|
172
|
+
that's 160K chars of tool-result content kept intact in the recent
|
|
173
|
+
window. Older tool_result blocks beyond this budget are eligible for
|
|
174
|
+
the lossy clear pass in `prune_history`.
|
|
181
175
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
at protect + PRUNE_MINIMUM = 55K + 20K = 75K chars (~19K tokens
|
|
187
|
-
of tool output), keeping the steady-state around 30K total.
|
|
176
|
+
Why flat (not scaled by model): opencode validated this in production
|
|
177
|
+
on contexts from 128K to 1M — scaling by ratio adds complexity without
|
|
178
|
+
improving behavior, and protecting too much in 1M-context models can
|
|
179
|
+
actually hurt prompt caching by keeping rarely-touched tail content warm.
|
|
188
180
|
|
|
189
181
|
The `model_id` parameter is retained for signature compatibility with
|
|
190
182
|
older call sites; it has no effect on the returned value.
|
|
191
183
|
"""
|
|
192
184
|
del model_id # unused — kept for signature compatibility
|
|
193
|
-
return
|
|
185
|
+
return 160_000
|
|
194
186
|
|
|
195
187
|
|
|
196
188
|
def prune_history(
|
|
@@ -204,6 +204,7 @@ TOOL_DISPLAY_NAMES = {
|
|
|
204
204
|
"list_directory": "List",
|
|
205
205
|
"bash": "Bash",
|
|
206
206
|
"rank_files": "Rank",
|
|
207
|
+
"delegate_task": "Agent",
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
TOOL_PRIMARY_ARG = {
|
|
@@ -216,6 +217,12 @@ TOOL_PRIMARY_ARG = {
|
|
|
216
217
|
"list_directory": "directory",
|
|
217
218
|
"bash": "command",
|
|
218
219
|
"rank_files": "task",
|
|
220
|
+
"delegate_task": "task",
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Agent type display names for delegate_task
|
|
224
|
+
_AGENT_TYPE_LABELS = {
|
|
225
|
+
"explorer": "Explorer",
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
|
|
@@ -225,6 +232,17 @@ def _format_tool_label(tool_name: str, tool_args: dict | None) -> str:
|
|
|
225
232
|
if not tool_args:
|
|
226
233
|
return display
|
|
227
234
|
|
|
235
|
+
# Special handling for delegate_task — show agent type and task summary
|
|
236
|
+
if tool_name == "delegate_task":
|
|
237
|
+
agent = str(tool_args.get("agent_name", "") or tool_args.get("agent", ""))
|
|
238
|
+
agent_label = _AGENT_TYPE_LABELS.get(agent, agent.title() if agent else "SubAgent")
|
|
239
|
+
task = str(tool_args.get("task", ""))
|
|
240
|
+
# Extract first meaningful line/sentence as summary
|
|
241
|
+
summary = task.split("\n")[0].strip()
|
|
242
|
+
if len(summary) > 60:
|
|
243
|
+
summary = summary[:57] + "..."
|
|
244
|
+
return f"{agent_label}({summary})" if summary else agent_label
|
|
245
|
+
|
|
228
246
|
primary_key = TOOL_PRIMARY_ARG.get(tool_name)
|
|
229
247
|
if primary_key and primary_key in tool_args:
|
|
230
248
|
value = str(tool_args[primary_key])
|
|
@@ -8,5 +8,6 @@ Public API for plugin authors:
|
|
|
8
8
|
|
|
9
9
|
from aru.plugins.tool_api import tool
|
|
10
10
|
from aru.plugins.hooks import Hooks, HookEvent, PluginInput
|
|
11
|
+
from aru.plugins.manager import PluginManager
|
|
11
12
|
|
|
12
|
-
__all__ = ["tool", "Hooks", "HookEvent", "PluginInput"]
|
|
13
|
+
__all__ = ["tool", "Hooks", "HookEvent", "PluginInput", "PluginManager"]
|