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.
Files changed (59) hide show
  1. {aru_code-0.17.0/aru_code.egg-info → aru_code-0.18.0}/PKG-INFO +1 -1
  2. aru_code-0.18.0/aru/__init__.py +1 -0
  3. {aru_code-0.17.0 → aru_code-0.18.0}/aru/agents/base.py +26 -11
  4. {aru_code-0.17.0 → aru_code-0.18.0}/aru/cli.py +5 -15
  5. {aru_code-0.17.0 → aru_code-0.18.0}/aru/completers.py +51 -9
  6. {aru_code-0.17.0 → aru_code-0.18.0}/aru/context.py +227 -93
  7. aru_code-0.18.0/aru/history_blocks.py +282 -0
  8. {aru_code-0.17.0 → aru_code-0.18.0}/aru/runner.py +102 -25
  9. {aru_code-0.17.0 → aru_code-0.18.0}/aru/session.py +45 -46
  10. {aru_code-0.17.0 → aru_code-0.18.0/aru_code.egg-info}/PKG-INFO +1 -1
  11. {aru_code-0.17.0 → aru_code-0.18.0}/aru_code.egg-info/SOURCES.txt +2 -0
  12. {aru_code-0.17.0 → aru_code-0.18.0}/pyproject.toml +1 -1
  13. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_agents_base.py +27 -1
  14. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli.py +100 -41
  15. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_completers.py +8 -2
  16. aru_code-0.18.0/tests/test_confabulation_regression.py +368 -0
  17. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_context.py +126 -5
  18. aru_code-0.17.0/aru/__init__.py +0 -1
  19. {aru_code-0.17.0 → aru_code-0.18.0}/LICENSE +0 -0
  20. {aru_code-0.17.0 → aru_code-0.18.0}/README.md +0 -0
  21. {aru_code-0.17.0 → aru_code-0.18.0}/aru/agent_factory.py +0 -0
  22. {aru_code-0.17.0 → aru_code-0.18.0}/aru/agents/__init__.py +0 -0
  23. {aru_code-0.17.0 → aru_code-0.18.0}/aru/agents/executor.py +0 -0
  24. {aru_code-0.17.0 → aru_code-0.18.0}/aru/agents/planner.py +0 -0
  25. {aru_code-0.17.0 → aru_code-0.18.0}/aru/cache_patch.py +0 -0
  26. {aru_code-0.17.0 → aru_code-0.18.0}/aru/commands.py +0 -0
  27. {aru_code-0.17.0 → aru_code-0.18.0}/aru/config.py +0 -0
  28. {aru_code-0.17.0 → aru_code-0.18.0}/aru/display.py +0 -0
  29. {aru_code-0.17.0 → aru_code-0.18.0}/aru/permissions.py +0 -0
  30. {aru_code-0.17.0 → aru_code-0.18.0}/aru/providers.py +0 -0
  31. {aru_code-0.17.0 → aru_code-0.18.0}/aru/runtime.py +0 -0
  32. {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/__init__.py +0 -0
  33. {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/ast_tools.py +0 -0
  34. {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/codebase.py +0 -0
  35. {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/gitignore.py +0 -0
  36. {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/mcp_client.py +0 -0
  37. {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/ranker.py +0 -0
  38. {aru_code-0.17.0 → aru_code-0.18.0}/aru/tools/tasklist.py +0 -0
  39. {aru_code-0.17.0 → aru_code-0.18.0}/aru_code.egg-info/dependency_links.txt +0 -0
  40. {aru_code-0.17.0 → aru_code-0.18.0}/aru_code.egg-info/entry_points.txt +0 -0
  41. {aru_code-0.17.0 → aru_code-0.18.0}/aru_code.egg-info/requires.txt +0 -0
  42. {aru_code-0.17.0 → aru_code-0.18.0}/aru_code.egg-info/top_level.txt +0 -0
  43. {aru_code-0.17.0 → aru_code-0.18.0}/setup.cfg +0 -0
  44. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_advanced.py +0 -0
  45. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_base.py +0 -0
  46. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_new.py +0 -0
  47. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_run_cli.py +0 -0
  48. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_session.py +0 -0
  49. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_cli_shell.py +0 -0
  50. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_codebase.py +0 -0
  51. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_config.py +0 -0
  52. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_executor.py +0 -0
  53. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_gitignore.py +0 -0
  54. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_main.py +0 -0
  55. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_mcp_client.py +0 -0
  56. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_permissions.py +0 -0
  57. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_planner.py +0 -0
  58. {aru_code-0.17.0 → aru_code-0.18.0}/tests/test_providers.py +0 -0
  59. {aru_code-0.17.0 → aru_code-0.18.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.17.0
3
+ Version: 0.18.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
@@ -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(path, query)` — returns a concise answer, not raw content
49
- 3. **Need raw content** → `read_file(path)` — returns first chunk + outline for large files
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(path, query)`
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(path, start_line=N, end_line=M)` using line numbers from grep
134
- 4. **Need the whole file?** → `read_file(path)` — returns first chunk + outline for large files
135
- 5. **Need the COMPLETE file (>60KB)?** → `read_file(path, max_size=0)` — reads in chunks. Use rarely.
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(path, query)` when you know the file.
175
- 2. **Know the file + have a question?** → `read_file_smart(path, query)`
176
- 3. **Need specific lines?** → `read_file(path, start_line=N, end_line=M)`
177
- 4. **Need the whole file?** → `read_file(path)` — returns first chunk + outline for large files.
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
- run_result = await run_agent_capture(agent, prompt, session, images=attached_images or None)
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
- run_result = await run_agent_capture(agent, prompt, session, images=attached_images or None)
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
- run_result = await run_agent_capture(agent, cmd_args or user_input, session, images=attached_images or None)
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
- run_result = await run_agent_capture(agent, message_text, session, images=attached_images or None)
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
- run_result = await run_agent_capture(agent, user_input, session, images=attached_images or None)
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[str, str]] # Simulated tool-call pairs for history
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 separate assistant+tool_result
41
- message pairs that the session can prune/compact like normal tool outputs.
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[str, str]] = []
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
- label += f" (truncated to {_MENTION_MAX_SIZE // 1000}KB of {size // 1000}KB — use read_file for the rest)"
84
- # Simulated tool call pair can be pruned like normal tool outputs
85
- file_messages.append({"role": "assistant", "content": label})
86
- file_messages.append({"role": "user", "content": content})
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