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.
- {aru_code-0.26.0/aru_code.egg-info → aru_code-0.26.1}/PKG-INFO +1 -1
- aru_code-0.26.1/aru/__init__.py +1 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/agent_factory.py +4 -40
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/runner.py +17 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/session.py +17 -0
- aru_code-0.26.1/aru/tools/plan_mode.py +169 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/tasklist.py +21 -2
- {aru_code-0.26.0 → aru_code-0.26.1/aru_code.egg-info}/PKG-INFO +1 -1
- {aru_code-0.26.0 → aru_code-0.26.1}/pyproject.toml +1 -1
- aru_code-0.26.0/aru/__init__.py +0 -1
- aru_code-0.26.0/aru/tools/plan_mode.py +0 -65
- {aru_code-0.26.0 → aru_code-0.26.1}/LICENSE +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/README.md +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/agents/__init__.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/agents/base.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/agents/catalog.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/agents/planner.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/cache_patch.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/checkpoints.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/cli.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/commands.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/completers.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/config.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/context.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/display.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/history_blocks.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/permissions.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/plugins/__init__.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/plugins/custom_tools.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/plugins/hooks.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/plugins/manager.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/plugins/tool_api.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/providers.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/runtime.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/__init__.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/codebase.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/gitignore.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru/tools/ranker.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru_code.egg-info/SOURCES.txt +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/setup.cfg +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_agents_base.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_catalog.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_checkpoints.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_base.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_completers.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_new.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_session.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_cli_shell.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_codebase.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_confabulation_regression.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_config.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_context.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_gitignore.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_guardrails_scenarios.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_main.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_mcp_client.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_permissions.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_plugins.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_providers.py +0 -0
- {aru_code-0.26.0 → aru_code-0.26.1}/tests/test_ranker.py +0 -0
|
@@ -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
|
|
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
|
-
|
|
191
|
-
|
|
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:
|
aru_code-0.26.0/aru/__init__.py
DELETED
|
@@ -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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|