aru-code 0.26.0__tar.gz → 0.26.1__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 (69) hide show
  1. {aru_code-0.26.0/aru_code.egg-info → aru_code-0.26.1}/PKG-INFO +1 -1
  2. aru_code-0.26.1/aru/__init__.py +1 -0
  3. {aru_code-0.26.0 → aru_code-0.26.1}/aru/agent_factory.py +4 -40
  4. {aru_code-0.26.0 → aru_code-0.26.1}/aru/runner.py +17 -0
  5. {aru_code-0.26.0 → aru_code-0.26.1}/aru/session.py +17 -0
  6. aru_code-0.26.1/aru/tools/plan_mode.py +169 -0
  7. {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/tasklist.py +21 -2
  8. {aru_code-0.26.0 → aru_code-0.26.1/aru_code.egg-info}/PKG-INFO +1 -1
  9. {aru_code-0.26.0 → aru_code-0.26.1}/pyproject.toml +1 -1
  10. aru_code-0.26.0/aru/__init__.py +0 -1
  11. aru_code-0.26.0/aru/tools/plan_mode.py +0 -65
  12. {aru_code-0.26.0 → aru_code-0.26.1}/LICENSE +0 -0
  13. {aru_code-0.26.0 → aru_code-0.26.1}/README.md +0 -0
  14. {aru_code-0.26.0 → aru_code-0.26.1}/aru/agents/__init__.py +0 -0
  15. {aru_code-0.26.0 → aru_code-0.26.1}/aru/agents/base.py +0 -0
  16. {aru_code-0.26.0 → aru_code-0.26.1}/aru/agents/catalog.py +0 -0
  17. {aru_code-0.26.0 → aru_code-0.26.1}/aru/agents/planner.py +0 -0
  18. {aru_code-0.26.0 → aru_code-0.26.1}/aru/cache_patch.py +0 -0
  19. {aru_code-0.26.0 → aru_code-0.26.1}/aru/checkpoints.py +0 -0
  20. {aru_code-0.26.0 → aru_code-0.26.1}/aru/cli.py +0 -0
  21. {aru_code-0.26.0 → aru_code-0.26.1}/aru/commands.py +0 -0
  22. {aru_code-0.26.0 → aru_code-0.26.1}/aru/completers.py +0 -0
  23. {aru_code-0.26.0 → aru_code-0.26.1}/aru/config.py +0 -0
  24. {aru_code-0.26.0 → aru_code-0.26.1}/aru/context.py +0 -0
  25. {aru_code-0.26.0 → aru_code-0.26.1}/aru/display.py +0 -0
  26. {aru_code-0.26.0 → aru_code-0.26.1}/aru/history_blocks.py +0 -0
  27. {aru_code-0.26.0 → aru_code-0.26.1}/aru/permissions.py +0 -0
  28. {aru_code-0.26.0 → aru_code-0.26.1}/aru/plugins/__init__.py +0 -0
  29. {aru_code-0.26.0 → aru_code-0.26.1}/aru/plugins/custom_tools.py +0 -0
  30. {aru_code-0.26.0 → aru_code-0.26.1}/aru/plugins/hooks.py +0 -0
  31. {aru_code-0.26.0 → aru_code-0.26.1}/aru/plugins/manager.py +0 -0
  32. {aru_code-0.26.0 → aru_code-0.26.1}/aru/plugins/tool_api.py +0 -0
  33. {aru_code-0.26.0 → aru_code-0.26.1}/aru/providers.py +0 -0
  34. {aru_code-0.26.0 → aru_code-0.26.1}/aru/runtime.py +0 -0
  35. {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/__init__.py +0 -0
  36. {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/ast_tools.py +0 -0
  37. {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/codebase.py +0 -0
  38. {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/gitignore.py +0 -0
  39. {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/mcp_client.py +0 -0
  40. {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/ranker.py +0 -0
  41. {aru_code-0.26.0 → aru_code-0.26.1}/aru_code.egg-info/SOURCES.txt +0 -0
  42. {aru_code-0.26.0 → aru_code-0.26.1}/aru_code.egg-info/dependency_links.txt +0 -0
  43. {aru_code-0.26.0 → aru_code-0.26.1}/aru_code.egg-info/entry_points.txt +0 -0
  44. {aru_code-0.26.0 → aru_code-0.26.1}/aru_code.egg-info/requires.txt +0 -0
  45. {aru_code-0.26.0 → aru_code-0.26.1}/aru_code.egg-info/top_level.txt +0 -0
  46. {aru_code-0.26.0 → aru_code-0.26.1}/setup.cfg +0 -0
  47. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_agents_base.py +0 -0
  48. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_catalog.py +0 -0
  49. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_checkpoints.py +0 -0
  50. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli.py +0 -0
  51. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_advanced.py +0 -0
  52. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_base.py +0 -0
  53. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_completers.py +0 -0
  54. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_new.py +0 -0
  55. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_run_cli.py +0 -0
  56. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_session.py +0 -0
  57. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_shell.py +0 -0
  58. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_codebase.py +0 -0
  59. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_confabulation_regression.py +0 -0
  60. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_config.py +0 -0
  61. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_context.py +0 -0
  62. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_gitignore.py +0 -0
  63. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_guardrails_scenarios.py +0 -0
  64. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_main.py +0 -0
  65. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_mcp_client.py +0 -0
  66. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_permissions.py +0 -0
  67. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_plugins.py +0 -0
  68. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_providers.py +0 -0
  69. {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_ranker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.26.0
3
+ Version: 0.26.1
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.26.1"
@@ -6,9 +6,6 @@ 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
-
12
9
  from aru.agents.base import build_instructions as _build_instructions
13
10
  from aru.agents.catalog import AGENTS, AgentSpec
14
11
  from aru.config import AgentConfig, CustomAgent
@@ -17,29 +14,6 @@ from aru.session import Session
17
14
 
18
15
  logger = logging.getLogger("aru.agent_factory")
19
16
 
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
-
43
17
 
44
18
  def _wrap_tools_with_hooks(tools: list) -> list:
45
19
  """Wrap tool functions to fire tool.execute.before/after plugin hooks.
@@ -175,16 +149,6 @@ def _apply_chat_hooks(instructions: str, model_ref: str, agent_name: str,
175
149
  return instructions, model_ref, max_tokens
176
150
 
177
151
 
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
152
  def create_agent_from_spec(
189
153
  spec: AgentSpec,
190
154
  session: Session | None = None,
@@ -194,8 +158,10 @@ def create_agent_from_spec(
194
158
  """Build an Agno Agent from a catalog spec.
195
159
 
196
160
  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.
161
+ Resolves model, wraps tools with plugin hooks, and applies chat.system.transform
162
+ and chat.params hooks. Context reduction is handled by aru's own layers
163
+ (`prune_history` for routine tool cleanup, `should_compact` near window limit),
164
+ so no Agno CompressionManager is attached.
199
165
 
200
166
  `session` may be None for subagent specs that always use the small model.
201
167
  """
@@ -222,8 +188,6 @@ def create_agent_from_spec(
222
188
  tools=tools,
223
189
  instructions=instructions,
224
190
  markdown=True,
225
- compress_tool_results=True,
226
- compression_manager=_make_compression_manager(),
227
191
  tool_call_limit=None,
228
192
  )
229
193
 
@@ -42,6 +42,14 @@ def _build_plan_reminder(session) -> str | None:
42
42
  if not steps:
43
43
  return None
44
44
 
45
+ # Auto-retire plans that have nothing left to execute. Leaving a fully-
46
+ # terminal plan in the reminder makes the agent re-surface it on the next
47
+ # turn — it may even call update_plan_step on old steps, re-rendering the
48
+ # stale panel and confusing the user who already moved on to a new task.
49
+ if all(s.status in ("completed", "skipped", "failed") for s in steps):
50
+ session.clear_plan()
51
+ return None
52
+
45
53
  pending = sum(1 for s in steps if s.status == "pending")
46
54
  done = sum(1 for s in steps if s.status == "completed")
47
55
  lines = [
@@ -411,6 +419,15 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
411
419
  # tool calls start a new round.
412
420
  if tool_result_msgs and tool_result_msgs[-1]["_open"]:
413
421
  tool_result_msgs[-1]["_open"] = False
422
+ # Flush coalesced plan-panel render. Multiple
423
+ # update_plan_step calls in the same batch (and any
424
+ # enter_plan_mode that replaces the plan mid-batch)
425
+ # collapse into a single panel showing final state.
426
+ try:
427
+ from aru.tools.tasklist import flush_plan_render
428
+ flush_plan_render(session)
429
+ except Exception:
430
+ pass
414
431
  live.update(display)
415
432
 
416
433
  elif isinstance(event, RunContentEvent):
@@ -176,6 +176,14 @@ class Session:
176
176
  # Transient flag set by runner when a turn ends with pending plan steps;
177
177
  # surfaced as a warning in the next turn's plan reminder, then cleared.
178
178
  self._pending_plan_warning: bool = False
179
+ # Monotonic plan generation — bumped whenever the plan is replaced or
180
+ # cleared. update_plan_step captures this and only its rendering loop
181
+ # consults it; lets the runner tell stale renders apart from live ones.
182
+ self._plan_generation: int = 0
183
+ # Set by update_plan_step / set_plan / clear_plan whenever plan state
184
+ # changes and a render should happen. Runner flushes this once per
185
+ # tool batch so multiple mutations in one batch produce one panel.
186
+ self._plan_render_pending: bool = False
179
187
  self.model_ref: str = DEFAULT_MODEL # provider/model format
180
188
  self.cwd: str = os.getcwd()
181
189
  self.created_at: str = datetime.now().isoformat(timespec="milliseconds")
@@ -230,13 +238,22 @@ class Session:
230
238
  self.current_plan = plan_content
231
239
  self.plan_task = task
232
240
  self.plan_steps = parse_plan_steps(plan_content)
241
+ self._plan_generation += 1
242
+ self._plan_render_pending = True
233
243
 
234
244
  def clear_plan(self):
235
245
  """Clear the active plan."""
246
+ had_plan = bool(self.plan_steps) or self.current_plan is not None
236
247
  self.current_plan = None
237
248
  self.plan_task = None
238
249
  self.plan_steps = []
239
250
  self._pending_plan_warning = False
251
+ if had_plan:
252
+ self._plan_generation += 1
253
+ # Clearing alone doesn't need a render — the replacement set_plan
254
+ # (or end-of-turn) will handle it. But mark pending so an explicit
255
+ # clear without a replacement still flushes any stale queued state.
256
+ self._plan_render_pending = False
240
257
 
241
258
  def track_tokens(self, metrics):
242
259
  """Accumulate token usage from a RunCompletedEvent.metrics."""
@@ -0,0 +1,169 @@
1
+ """Plan mode control surface — agent-invokable tool to generate a structured plan.
2
+
3
+ This is the autonomous counterpart to the `/plan` slash command. The build
4
+ agent calls `enter_plan_mode(task)` when it detects a request requiring
5
+ multiple coordinated changes; the tool runs the planner via runner.prompt,
6
+ stores the plan in the session, and returns a summary so the build agent can
7
+ immediately follow the resulting PLAN ACTIVE reminder.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+
14
+ from rich.panel import Panel
15
+
16
+ from aru.runtime import get_ctx
17
+
18
+
19
+ def _prompt_plan_approval(plan_steps: list, n_steps: int) -> tuple[bool, str]:
20
+ """Show the plan panel and ask the user to approve execution.
21
+
22
+ Returns (approved, feedback). If the user types free text instead of y/n,
23
+ it is treated as feedback that the agent should use to adjust course.
24
+ Non-interactive sessions (no TTY) auto-approve.
25
+ """
26
+ # Auto-approve in non-interactive sessions — there's nobody to answer.
27
+ if not sys.stdin.isatty():
28
+ return True, ""
29
+
30
+ from aru.tools.tasklist import _render_plan_steps
31
+
32
+ ctx = get_ctx()
33
+
34
+ if ctx.live:
35
+ ctx.live.stop()
36
+ if ctx.display:
37
+ try:
38
+ ctx.display.flush()
39
+ except Exception:
40
+ pass
41
+
42
+ ctx.console.print()
43
+ ctx.console.print(_render_plan_steps(plan_steps))
44
+ ctx.console.print(Panel(
45
+ f"Proposed plan with [bold]{n_steps}[/bold] steps. Approve execution?",
46
+ title="[bold cyan]Plan approval[/bold cyan]",
47
+ border_style="cyan",
48
+ expand=False,
49
+ ))
50
+
51
+ try:
52
+ answer = ctx.console.input(
53
+ "[bold cyan](y)es / (n)o / type feedback to revise:[/bold cyan] "
54
+ ).strip()
55
+ except (EOFError, KeyboardInterrupt):
56
+ answer = "n"
57
+ finally:
58
+ if ctx.live:
59
+ try:
60
+ ctx.live.start()
61
+ ctx.live._live_render._shape = None
62
+ except Exception:
63
+ pass
64
+
65
+ low = answer.lower()
66
+ if not answer or low in ("y", "yes", "s", "sim", "ok"):
67
+ return True, ""
68
+ if low in ("n", "no", "não", "nao"):
69
+ return False, ""
70
+ return False, answer
71
+
72
+
73
+ async def enter_plan_mode(task: str, force: bool = False) -> str:
74
+ """Generate a structured plan for a complex task and get user approval.
75
+
76
+ Use this when the user asks for work that requires 3+ coordinated changes
77
+ across files, or when they explicitly ask for a new plan. Generates a
78
+ read-only plan via the planner agent, shows it to the user, and asks for
79
+ explicit approval before execution proceeds.
80
+
81
+ IMPORTANT: the plan is NOT automatically executed. After this tool
82
+ returns, one of three things happened:
83
+ 1. User approved — tool returns the plan and you should execute it,
84
+ calling update_plan_step(index, "completed") as you finish each step.
85
+ 2. User rejected — tool returns a rejection message. Stop, do NOT
86
+ execute, and ask the user what they want instead.
87
+ 3. User gave free-text feedback — tool returns the feedback. Stop,
88
+ do NOT execute, and either replan (enter_plan_mode again with the
89
+ revised task) or discuss with the user.
90
+
91
+ Behavior with an existing plan:
92
+ - If the previous plan is fully terminal (all steps done/skipped/
93
+ failed), it is automatically replaced.
94
+ - If the previous plan still has pending or in-progress steps, this
95
+ tool refuses UNLESS you pass force=True. Only pass force=True when
96
+ the user explicitly asked for a new plan. Do NOT call
97
+ update_plan_step to "close out" stale steps before replanning.
98
+
99
+ Args:
100
+ task: One-line description of what to plan.
101
+ force: Pass True to replace a plan that still has unfinished steps.
102
+ """
103
+ ctx = get_ctx()
104
+ session = ctx.session
105
+ if session is None:
106
+ return "Error: enter_plan_mode requires an active session."
107
+
108
+ existing_steps = getattr(session, "plan_steps", None) or []
109
+ if existing_steps:
110
+ unfinished = [s for s in existing_steps if s.status not in ("completed", "skipped", "failed")]
111
+ if unfinished and not force:
112
+ pending_list = ", ".join(f"#{s.index}" for s in unfinished)
113
+ return (
114
+ f"Error: a plan is already active with {len(unfinished)} unfinished "
115
+ f"step(s) ({pending_list}). If the user explicitly asked for a new "
116
+ f"plan, retry with force=True to discard the in-progress plan. Do "
117
+ f"NOT call update_plan_step to close out the old steps — that only "
118
+ f"re-renders the stale plan. Otherwise, execute the existing plan "
119
+ f"(see the PLAN ACTIVE reminder)."
120
+ )
121
+ session.clear_plan()
122
+
123
+ from aru.runner import PromptInput, prompt as runner_prompt
124
+
125
+ result = await runner_prompt(PromptInput(
126
+ session=session,
127
+ message=task,
128
+ agent_name="plan",
129
+ lightweight=True,
130
+ ))
131
+ plan_content = (result.content or "").strip()
132
+ if not plan_content:
133
+ return "Error: planner returned no content. Aborting plan_mode."
134
+
135
+ session.set_plan(task, plan_content)
136
+ n_steps = len(session.plan_steps)
137
+ if n_steps == 0:
138
+ return (
139
+ f"Plan generated but no steps were detected. The next turn will not "
140
+ f"see a PLAN ACTIVE reminder — execute manually based on this plan:\n\n"
141
+ f"{plan_content}"
142
+ )
143
+
144
+ approved, feedback = _prompt_plan_approval(session.plan_steps, n_steps)
145
+
146
+ # The approval prompt already rendered the plan panel inline, so suppress
147
+ # the runner's coalesced end-of-batch render to avoid a duplicate.
148
+ session._plan_render_pending = False
149
+
150
+ if not approved:
151
+ session.clear_plan()
152
+ if feedback:
153
+ return (
154
+ f"User rejected the plan and gave this feedback:\n\n {feedback}\n\n"
155
+ f"Do NOT execute anything. Either call enter_plan_mode again with a "
156
+ f"revised task that incorporates the feedback, or ask the user for "
157
+ f"clarification. The previous plan has been discarded."
158
+ )
159
+ return (
160
+ "User rejected the plan. Do NOT execute anything. Ask the user what "
161
+ "they would like to change, then optionally call enter_plan_mode "
162
+ "again with a revised task. The previous plan has been discarded."
163
+ )
164
+
165
+ return (
166
+ f"User approved the plan ({n_steps} steps). Execute the steps in order "
167
+ f"and call update_plan_step(index, 'completed') after each.\n\n"
168
+ f"--- PLAN ---\n{plan_content}"
169
+ )
@@ -133,6 +133,22 @@ def update_task(index: int, status: str) -> str:
133
133
  _PLAN_STATUSES = ("in_progress", "completed", "failed", "skipped")
134
134
 
135
135
 
136
+ def flush_plan_render(session) -> None:
137
+ """Render the plan panel once if the session has a pending update.
138
+
139
+ Called by the runner after each tool batch. Coalesces multiple
140
+ update_plan_step mutations into a single visible panel and ensures that
141
+ if enter_plan_mode replaced the plan mid-batch, only the new plan shows.
142
+ """
143
+ if session is None or not getattr(session, "_plan_render_pending", False):
144
+ return
145
+ session._plan_render_pending = False
146
+ steps = getattr(session, "plan_steps", None)
147
+ if not steps:
148
+ return
149
+ _show(_render_plan_steps(steps))
150
+
151
+
136
152
  def _render_plan_steps(steps: list) -> Panel:
137
153
  """Render the macro plan_steps list as a Rich panel."""
138
154
  icons = {
@@ -187,8 +203,11 @@ def update_plan_step(index: int, status: str) -> str:
187
203
  return f"Error: Plan step {index} not found. Valid indices: {valid}."
188
204
 
189
205
  target.status = status
190
- panel = _render_plan_steps(session.plan_steps)
191
- _show(panel)
206
+ # Defer rendering: mark the session so the runner flushes a single plan
207
+ # panel at the end of the current tool batch. Rendering per-call causes
208
+ # stale plans to reappear when enter_plan_mode is called in the same
209
+ # batch (the old plan renders, then gets replaced moments later).
210
+ session._plan_render_pending = True
192
211
 
193
212
  pending = [s for s in session.plan_steps if s.status == "pending"]
194
213
  if not pending:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.26.0
3
+ Version: 0.26.1
4
4
  Summary: A Claude Code clone built with Agno agents
5
5
  Author-email: Estevao <estevaofon@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aru-code"
7
- version = "0.26.0"
7
+ version = "0.26.1"
8
8
  description = "A Claude Code clone built with Agno agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "0.26.0"
@@ -1,65 +0,0 @@
1
- """Plan mode control surface — agent-invokable tool to generate a structured plan.
2
-
3
- This is the autonomous counterpart to the `/plan` slash command. The build
4
- agent calls `enter_plan_mode(task)` when it detects a request requiring
5
- multiple coordinated changes; the tool runs the planner via runner.prompt,
6
- stores the plan in the session, and returns a summary so the build agent can
7
- immediately follow the resulting PLAN ACTIVE reminder.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from aru.runtime import get_ctx
13
-
14
-
15
- async def enter_plan_mode(task: str) -> str:
16
- """Generate a structured plan for a complex task before executing.
17
-
18
- Use this when the user asks for work that requires 3+ coordinated changes
19
- across files. Generates a read-only plan via the planner agent, stores it
20
- in the session, and returns the plan text. After this returns, a PLAN
21
- ACTIVE system reminder will appear in your context — follow it: execute
22
- each step in order and call update_plan_step(index, "completed") as you go.
23
-
24
- Do NOT call this if a plan is already active — execute the existing plan.
25
-
26
- Args:
27
- task: One-line description of what to plan.
28
- """
29
- ctx = get_ctx()
30
- session = ctx.session
31
- if session is None:
32
- return "Error: enter_plan_mode requires an active session."
33
-
34
- if getattr(session, "plan_steps", None):
35
- return (
36
- "Error: a plan is already active. Execute the existing plan steps "
37
- "(see the PLAN ACTIVE reminder) instead of replanning."
38
- )
39
-
40
- from aru.runner import PromptInput, prompt as runner_prompt
41
-
42
- result = await runner_prompt(PromptInput(
43
- session=session,
44
- message=task,
45
- agent_name="plan",
46
- lightweight=True,
47
- ))
48
- plan_content = (result.content or "").strip()
49
- if not plan_content:
50
- return "Error: planner returned no content. Aborting plan_mode."
51
-
52
- session.set_plan(task, plan_content)
53
- n_steps = len(session.plan_steps)
54
- if n_steps == 0:
55
- return (
56
- f"Plan generated but no steps were detected. The next turn will not "
57
- f"see a PLAN ACTIVE reminder — execute manually based on this plan:\n\n"
58
- f"{plan_content}"
59
- )
60
- return (
61
- f"Plan stored: {n_steps} steps. The PLAN ACTIVE reminder will appear in "
62
- f"your next context window — execute steps in order and call "
63
- f"update_plan_step(index, 'completed') after each.\n\n"
64
- f"--- PLAN ---\n{plan_content}"
65
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes