aru-code 0.17.0__tar.gz → 0.18.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.17.0/aru_code.egg-info → aru_code-0.18.0}/PKG-INFO +1 -1
- aru_code-0.18.0/aru/__init__.py +1 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/agents/base.py +26 -11
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/cli.py +5 -15
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/completers.py +51 -9
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/context.py +227 -93
- aru_code-0.18.0/aru/history_blocks.py +282 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/runner.py +102 -25
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/session.py +45 -46
- {aru_code-0.17.0 → aru_code-0.18.0/aru_code.egg-info}/PKG-INFO +1 -1
- {aru_code-0.17.0 → aru_code-0.18.0}/aru_code.egg-info/SOURCES.txt +2 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/pyproject.toml +1 -1
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_agents_base.py +27 -1
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli.py +100 -41
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_completers.py +8 -2
- aru_code-0.18.0/tests/test_confabulation_regression.py +368 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_context.py +126 -5
- aru_code-0.17.0/aru/__init__.py +0 -1
- {aru_code-0.17.0 → aru_code-0.18.0}/LICENSE +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/README.md +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/agent_factory.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/agents/executor.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/agents/planner.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/cache_patch.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/commands.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/config.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/display.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/permissions.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/providers.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/runtime.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/codebase.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/setup.cfg +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_codebase.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_config.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_executor.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_main.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_permissions.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_planner.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_providers.py +0 -0
- {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_ranker.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.18.0"
|
|
@@ -27,7 +27,22 @@ NEVER create documentation files (*.md) unless the user explicitly asks for them
|
|
|
27
27
|
Focus on writing working code, not documentation.
|
|
28
28
|
Deliver EXACTLY what was asked — no more, no less. \
|
|
29
29
|
One function requested = one function written. Helper functions, tests, utilities, and "while I'm here" \
|
|
30
|
-
improvements are out of scope unless the user names them explicitly
|
|
30
|
+
improvements are out of scope unless the user names them explicitly.
|
|
31
|
+
|
|
32
|
+
## Reasoning rules
|
|
33
|
+
|
|
34
|
+
**Verify before asserting.** If you describe what a function, module, or system does, \
|
|
35
|
+
you must have actually read the relevant code in this conversation. Inferring behavior \
|
|
36
|
+
from a call site, function name, or adjacent code counts as hallucination — "it probably \
|
|
37
|
+
does X" is not a valid source. When you are about to make a claim about unread code, \
|
|
38
|
+
stop and `grep_search` or `read_file` first. Reading is cheaper than being wrong.
|
|
39
|
+
|
|
40
|
+
**Adopt user scope corrections immediately.** When the user redirects the conversation \
|
|
41
|
+
("actually, look at X instead", "that one is a different context", "o scheduler que eu \
|
|
42
|
+
disse é Y"), drop the previous frame completely. Do not hedge with caveats about the \
|
|
43
|
+
earlier topic ("Porém, se também considerarmos...") unless the user explicitly asks for \
|
|
44
|
+
them. The user's correction is authoritative — respond as if the earlier framing never \
|
|
45
|
+
happened.\
|
|
31
46
|
"""
|
|
32
47
|
|
|
33
48
|
# Planner-specific additions (read-only exploration + output format)
|
|
@@ -45,8 +60,8 @@ Every tool call accumulates its result in your context window. Use the minimum n
|
|
|
45
60
|
|
|
46
61
|
1. **Find files/patterns** → `grep_search(pattern, file_glob="*.py")` or `glob_search`. \
|
|
47
62
|
Default shows 10 lines of context — use `context_lines=30` for full function bodies.
|
|
48
|
-
2. **Understand a file** → `read_file_smart(
|
|
49
|
-
3. **Need raw content** → `read_file(
|
|
63
|
+
2. **Understand a file** → `read_file_smart(file_path, query)` — returns a concise answer, not raw content
|
|
64
|
+
3. **Need raw content** → `read_file(file_path)` — returns first chunk + outline for large files
|
|
50
65
|
|
|
51
66
|
**Batch independent tool calls**: When you need answers from multiple independent sources, \
|
|
52
67
|
emit ALL those tool calls in a single response.
|
|
@@ -127,12 +142,12 @@ split into subtasks grouped by concern (e.g. "Create model files", "Create route
|
|
|
127
142
|
|
|
128
143
|
## Reading strategy — read, edit, test
|
|
129
144
|
|
|
130
|
-
1. **Know the file + have a question?** → `read_file_smart(
|
|
145
|
+
1. **Know the file + have a question?** → `read_file_smart(file_path, query)`
|
|
131
146
|
2. **Need a specific pattern?** → `grep_search(pattern, file_glob="*.py")` — default 10 lines context. \
|
|
132
147
|
Use `context_lines=30` for full function bodies.
|
|
133
|
-
3. **Need lines for editing?** → `read_file(
|
|
134
|
-
4. **Need the whole file?** → `read_file(
|
|
135
|
-
5. **Need the COMPLETE file (>60KB)?** → `read_file(
|
|
148
|
+
3. **Need lines for editing?** → `read_file(file_path, start_line=N, end_line=M)` using line numbers from grep
|
|
149
|
+
4. **Need the whole file?** → `read_file(file_path)` — returns first chunk + outline for large files
|
|
150
|
+
5. **Need the COMPLETE file (>60KB)?** → `read_file(file_path, max_size=0)` — reads in chunks. Use rarely.
|
|
136
151
|
|
|
137
152
|
**NEVER read the same file twice.** If you already have the file content in context, use it.
|
|
138
153
|
|
|
@@ -171,10 +186,10 @@ Skip exploration when the task is clear and the relevant files are obvious.
|
|
|
171
186
|
Every tool call accumulates its result in your context window. Use the minimum needed:
|
|
172
187
|
|
|
173
188
|
1. **Don't know which file?** → `grep_search` / `glob_search` for patterns, \
|
|
174
|
-
`read_file_smart(
|
|
175
|
-
2. **Know the file + have a question?** → `read_file_smart(
|
|
176
|
-
3. **Need specific lines?** → `read_file(
|
|
177
|
-
4. **Need the whole file?** → `read_file(
|
|
189
|
+
`read_file_smart(file_path, query)` when you know the file.
|
|
190
|
+
2. **Know the file + have a question?** → `read_file_smart(file_path, query)`
|
|
191
|
+
3. **Need specific lines?** → `read_file(file_path, start_line=N, end_line=M)`
|
|
192
|
+
4. **Need the whole file?** → `read_file(file_path)` — returns first chunk + outline for large files.
|
|
178
193
|
|
|
179
194
|
**NEVER read the same file twice.** Check if you already have the content in context.
|
|
180
195
|
|
|
@@ -497,9 +497,7 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
497
497
|
else:
|
|
498
498
|
agent = create_general_agent(session, config, env_context=env_ctx)
|
|
499
499
|
session.add_message("user", user_input)
|
|
500
|
-
|
|
501
|
-
if run_result.content:
|
|
502
|
-
session.add_message("assistant", run_result.with_tools_summary())
|
|
500
|
+
await run_agent_capture(agent, prompt, session, images=attached_images or None)
|
|
503
501
|
elif cmd_name in config.skills:
|
|
504
502
|
skill = config.skills[cmd_name]
|
|
505
503
|
if not skill.user_invocable:
|
|
@@ -510,9 +508,7 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
510
508
|
|
|
511
509
|
agent = create_general_agent(session, config, env_context=_build_env_ctx())
|
|
512
510
|
session.add_message("user", user_input)
|
|
513
|
-
|
|
514
|
-
if run_result.content:
|
|
515
|
-
session.add_message("assistant", run_result.with_tools_summary())
|
|
511
|
+
await run_agent_capture(agent, prompt, session, images=attached_images or None)
|
|
516
512
|
elif cmd_name in config.custom_agents:
|
|
517
513
|
agent_def = config.custom_agents[cmd_name]
|
|
518
514
|
if agent_def.mode == "subagent":
|
|
@@ -523,9 +519,7 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
523
519
|
agent = create_custom_agent_instance(agent_def, session, config, env_context=_build_env_ctx())
|
|
524
520
|
session.add_message("user", user_input)
|
|
525
521
|
with permission_scope(agent_def.permission):
|
|
526
|
-
|
|
527
|
-
if run_result.content:
|
|
528
|
-
session.add_message("assistant", run_result.with_tools_summary())
|
|
522
|
+
await run_agent_capture(agent, cmd_args or user_input, session, images=attached_images or None)
|
|
529
523
|
else:
|
|
530
524
|
console.print(f"[yellow]Unknown command: /{cmd_name}[/yellow]")
|
|
531
525
|
console.print(f"[dim]Built-in: /plan, /model, /sessions, /commands, /skills, /agents, /cost, /quit[/dim]")
|
|
@@ -551,15 +545,11 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
551
545
|
agent = create_custom_agent_instance(agent_def, session, config, env_context=_build_env_ctx())
|
|
552
546
|
session.add_message("user", user_input)
|
|
553
547
|
with permission_scope(agent_def.permission):
|
|
554
|
-
|
|
555
|
-
if run_result.content:
|
|
556
|
-
session.add_message("assistant", run_result.with_tools_summary())
|
|
548
|
+
await run_agent_capture(agent, message_text, session, images=attached_images or None)
|
|
557
549
|
else:
|
|
558
550
|
agent = create_general_agent(session, config, env_context=_build_env_ctx())
|
|
559
551
|
session.add_message("user", user_input)
|
|
560
|
-
|
|
561
|
-
if run_result.content:
|
|
562
|
-
session.add_message("assistant", run_result.with_tools_summary())
|
|
552
|
+
await run_agent_capture(agent, user_input, session, images=attached_images or None)
|
|
563
553
|
|
|
564
554
|
# Show token usage and auto-save
|
|
565
555
|
if session.token_summary:
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import inspect
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
from dataclasses import dataclass
|
|
9
|
+
from functools import lru_cache
|
|
8
10
|
|
|
9
11
|
from prompt_toolkit import PromptSession
|
|
10
12
|
from prompt_toolkit.completion import Completer, Completion
|
|
@@ -24,11 +26,28 @@ _IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
|
|
|
24
26
|
_IMAGE_MAX_SIZE = 20 * 1024 * 1024 # 20MB
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
@lru_cache(maxsize=1)
|
|
30
|
+
def _read_file_arg_name() -> str:
|
|
31
|
+
"""Return the name of the first parameter of the real `read_file` tool.
|
|
32
|
+
|
|
33
|
+
Used by `_resolve_mentions` to forge tool_use blocks whose `input` dict
|
|
34
|
+
key matches the real tool signature exactly. If `read_file` is ever
|
|
35
|
+
renamed or its signature changes, this introspection picks up the new
|
|
36
|
+
name automatically — eliminating the entire class of "forged example
|
|
37
|
+
drifted from real tool" bugs.
|
|
38
|
+
|
|
39
|
+
Cached via lru_cache so the introspection runs once per process.
|
|
40
|
+
Imported lazily to avoid any import-time coupling with aru.tools.
|
|
41
|
+
"""
|
|
42
|
+
from aru.tools.codebase import read_file
|
|
43
|
+
return next(iter(inspect.signature(read_file).parameters))
|
|
44
|
+
|
|
45
|
+
|
|
27
46
|
@dataclass
|
|
28
47
|
class MentionResult:
|
|
29
48
|
"""Result of resolving @file mentions."""
|
|
30
49
|
text: str # User text (without file contents)
|
|
31
|
-
file_messages: list[dict
|
|
50
|
+
file_messages: list[dict] # Block-shaped tool_use/tool_result pairs for history
|
|
32
51
|
images: list[Image]
|
|
33
52
|
count: int # Total attached (files + images)
|
|
34
53
|
|
|
@@ -37,20 +56,25 @@ def _resolve_mentions(text: str, cwd: str, agent_names: set[str] | None = None)
|
|
|
37
56
|
"""Resolve @file mentions as simulated read_file tool calls.
|
|
38
57
|
|
|
39
58
|
Instead of inlining file contents into the user message (which bloats
|
|
40
|
-
history and can't be pruned), we return
|
|
41
|
-
message pairs
|
|
59
|
+
history and can't be pruned), we return real block-shaped tool_use /
|
|
60
|
+
tool_result message pairs with synthetic tool_use_ids. The prune
|
|
61
|
+
pipeline in `aru.context.prune_history` then treats them as atomic
|
|
62
|
+
pairs — label and content can't be cut apart.
|
|
42
63
|
|
|
43
64
|
Image files are returned as Image objects.
|
|
44
65
|
Skips @mentions that match known agent names.
|
|
45
66
|
"""
|
|
67
|
+
from aru.history_blocks import tool_use_block, tool_result_block
|
|
68
|
+
|
|
46
69
|
agent_names = agent_names or set()
|
|
47
70
|
matches = list(_MENTION_RE.finditer(text))
|
|
48
71
|
if not matches:
|
|
49
72
|
return MentionResult(text=text, file_messages=[], images=[], count=0)
|
|
50
73
|
|
|
51
|
-
file_messages: list[dict
|
|
74
|
+
file_messages: list[dict] = []
|
|
52
75
|
images: list[Image] = []
|
|
53
76
|
seen = set()
|
|
77
|
+
mention_idx = 0
|
|
54
78
|
for m in matches:
|
|
55
79
|
rel_path = m.group(1)
|
|
56
80
|
if rel_path.lower() in agent_names:
|
|
@@ -78,12 +102,30 @@ def _resolve_mentions(text: str, cwd: str, agent_names: set[str] | None = None)
|
|
|
78
102
|
with open(abs_path, "r", encoding="utf-8", errors="replace") as f:
|
|
79
103
|
content = f.read(_MENTION_MAX_SIZE)
|
|
80
104
|
truncated = size > _MENTION_MAX_SIZE
|
|
81
|
-
label = f"[read_file: {rel_path}]"
|
|
82
105
|
if truncated:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
106
|
+
content += (
|
|
107
|
+
f"\n\n[truncated to {_MENTION_MAX_SIZE // 1000}KB of "
|
|
108
|
+
f"{size // 1000}KB — use read_file for the rest]"
|
|
109
|
+
)
|
|
110
|
+
# Synthetic tool_use_id so the prune pipeline can pair the
|
|
111
|
+
# assistant tool_use block with its matching tool_result.
|
|
112
|
+
# The `input` dict key is derived at runtime from the real
|
|
113
|
+
# read_file signature via `_read_file_arg_name()` — this makes
|
|
114
|
+
# the forgery drift-proof: if `read_file` is ever renamed or
|
|
115
|
+
# its first parameter changes, the forged example automatically
|
|
116
|
+
# tracks the new name. Without this, the model would see its
|
|
117
|
+
# own prior "tool calls" in history using a stale arg name and
|
|
118
|
+
# copy that stale pattern, producing Pydantic validation errors.
|
|
119
|
+
tu_id = f"mention_{mention_idx}_{abs(hash(rel_path)) & 0xFFFFFF:06x}"
|
|
120
|
+
mention_idx += 1
|
|
121
|
+
file_messages.append({
|
|
122
|
+
"role": "assistant",
|
|
123
|
+
"content": [tool_use_block(tu_id, "read_file", {_read_file_arg_name(): rel_path})],
|
|
124
|
+
})
|
|
125
|
+
file_messages.append({
|
|
126
|
+
"role": "tool",
|
|
127
|
+
"content": [tool_result_block(tu_id, content)],
|
|
128
|
+
})
|
|
87
129
|
except OSError:
|
|
88
130
|
continue
|
|
89
131
|
|