aru-code 0.32.0__tar.gz → 0.33.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.32.0 → aru_code-0.33.0}/PKG-INFO +1 -1
- aru_code-0.33.0/aru/__init__.py +1 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/agent_factory.py +9 -1
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/agents/base.py +94 -1
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/agents/catalog.py +63 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/cli.py +32 -1
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/commands.py +132 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/permissions.py +318 -21
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/runtime.py +78 -1
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/session.py +87 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tool_policy.py +75 -49
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/codebase.py +1 -1
- aru_code-0.33.0/aru/tools/delegate.py +602 -0
- aru_code-0.33.0/aru/tools/delegate_prompt.txt +34 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/file_ops.py +2 -2
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/registry.py +10 -5
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/skill.py +1 -1
- {aru_code-0.32.0 → aru_code-0.33.0}/aru_code.egg-info/PKG-INFO +1 -1
- {aru_code-0.32.0 → aru_code-0.33.0}/aru_code.egg-info/SOURCES.txt +2 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/pyproject.toml +4 -1
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_catalog.py +8 -1
- aru_code-0.33.0/tests/test_delegate.py +1063 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_invoke_skill.py +4 -4
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_permissions.py +501 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_tool_policy.py +88 -0
- aru_code-0.32.0/aru/__init__.py +0 -1
- aru_code-0.32.0/aru/tools/delegate.py +0 -236
- {aru_code-0.32.0 → aru_code-0.33.0}/LICENSE +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/README.md +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/agents/planner.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/cache_patch.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/checkpoints.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/completers.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/config.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/context.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/display.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/history_blocks.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/plugin_cache.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/plugins/__init__.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/plugins/custom_tools.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/plugins/hooks.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/plugins/manager.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/plugins/tool_api.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/providers.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/runner.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/select.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/_diff.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/_shared.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/plan_mode.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/search.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/shell.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/web.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/setup.cfg +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_agents_md_coverage.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cache_patch_metrics.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cache_patch_stop_reason.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_checkpoints.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_codebase.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_confabulation_regression.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_config.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_context.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_guardrails_scenarios.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_invoked_skills.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_main.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_microcompact.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_plan_mode_refactor.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_plugin_cache.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_plugins.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_providers.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_ranker.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_reasoning.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_runner_recovery.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_runtime.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_select.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_skill_disallowed_tools.py +0 -0
- {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_tasklist.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.33.0"
|
|
@@ -150,7 +150,15 @@ async def create_agent_from_spec(
|
|
|
150
150
|
resolved_model = model_ref or session.model_ref
|
|
151
151
|
|
|
152
152
|
tools = _wrap_tools_with_hooks(spec.tools_factory())
|
|
153
|
-
instructions
|
|
153
|
+
# Merge spec-level extra instructions (static, agent-specific policy like
|
|
154
|
+
# "you are read-only, never call write tools") with caller-provided extras
|
|
155
|
+
# (dynamic, session-specific context like cwd or AGENTS.md). Spec text
|
|
156
|
+
# comes first so the agent's baseline policy is established before any
|
|
157
|
+
# session-specific text that might try to override it.
|
|
158
|
+
combined_extra = "\n\n".join(
|
|
159
|
+
part for part in (spec.extra_instructions, extra_instructions) if part
|
|
160
|
+
)
|
|
161
|
+
instructions = _build_instructions(spec.role, combined_extra)
|
|
154
162
|
|
|
155
163
|
instructions, resolved_model, max_tokens = await _apply_chat_hooks(
|
|
156
164
|
instructions, resolved_model, spec.name, max_tokens=spec.max_tokens,
|
|
@@ -374,11 +374,101 @@ Complete the search request efficiently and report your findings clearly.\
|
|
|
374
374
|
"""
|
|
375
375
|
|
|
376
376
|
|
|
377
|
+
VERIFIER_ROLE = """\
|
|
378
|
+
You are a verification sub-agent. Your sole job is to review a recent batch
|
|
379
|
+
of edits for correctness and report issues.
|
|
380
|
+
|
|
381
|
+
=== CRITICAL: READ-ONLY MODE — NO FILE MODIFICATIONS ===
|
|
382
|
+
You are STRICTLY PROHIBITED from creating, editing, deleting, or moving
|
|
383
|
+
files. You do not have access to edit tools; attempts will fail. No
|
|
384
|
+
state-changing bash commands (no git add/commit, no npm/pip install, no
|
|
385
|
+
mkdir/touch/rm/cp/mv).
|
|
386
|
+
|
|
387
|
+
Your workflow:
|
|
388
|
+
1. Read each file mentioned in the task using `read_file` or `read_files`
|
|
389
|
+
2. Search for call sites / references to changed APIs using `grep_search`
|
|
390
|
+
3. Skim related tests using `glob_search` + `read_file`
|
|
391
|
+
4. Report findings in this structure:
|
|
392
|
+
- Inconsistencies found (with file:line refs)
|
|
393
|
+
- Missing follow-up edits (call sites not updated, etc.)
|
|
394
|
+
- Suspicious patterns worth the caller's attention (even if uncertain)
|
|
395
|
+
- What looks correct (brief — don't pad the report)
|
|
396
|
+
|
|
397
|
+
Be concise. Skip nitpicks (formatting, naming preferences). Focus on
|
|
398
|
+
bugs, broken contracts, or outdated call sites the caller likely missed.
|
|
399
|
+
|
|
400
|
+
Return ONE final message. The caller is not able to ask follow-ups
|
|
401
|
+
without a resume — include everything they need to act.\
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
REVIEWER_ROLE = """\
|
|
406
|
+
You are a code-review sub-agent. Review the files mentioned in the task
|
|
407
|
+
against common quality heuristics and produce actionable findings.
|
|
408
|
+
|
|
409
|
+
=== CRITICAL: READ-ONLY MODE — NO FILE MODIFICATIONS ===
|
|
410
|
+
You may only read and search. No edit/write/delete/move operations. No
|
|
411
|
+
state-changing bash.
|
|
412
|
+
|
|
413
|
+
For each file covered:
|
|
414
|
+
|
|
415
|
+
- Naming: are identifiers clear and consistent with the surrounding code?
|
|
416
|
+
- Error handling: are edge cases covered? Any swallowed exceptions?
|
|
417
|
+
- Testing: is there test coverage for the new/modified code paths?
|
|
418
|
+
- Security: obvious injection, path traversal, secret exposure, unchecked
|
|
419
|
+
user input, missing auth checks?
|
|
420
|
+
- Complexity: functions that should be split, duplicated logic, over-
|
|
421
|
+
engineered abstractions for simple cases?
|
|
422
|
+
|
|
423
|
+
Report format:
|
|
424
|
+
- One bullet per finding
|
|
425
|
+
- Include file:line
|
|
426
|
+
- Classify severity: (blocker) / (important) / (nit) — omit (nit) unless
|
|
427
|
+
asked for a thorough review
|
|
428
|
+
- If nothing is wrong, say so plainly — do not fabricate issues
|
|
429
|
+
|
|
430
|
+
Return ONE final message covering every file you looked at.\
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
GUIDE_ROLE = """\
|
|
435
|
+
You are the Aru user-guide sub-agent. You answer questions about how to
|
|
436
|
+
use and configure Aru itself — slash commands, permission config, skills,
|
|
437
|
+
plugins, tool catalog, session management.
|
|
438
|
+
|
|
439
|
+
The questions are about Aru, NOT about the user's own codebase. When in
|
|
440
|
+
doubt, treat the task as "explain how to do X with Aru" rather than "do X
|
|
441
|
+
in the user's project".
|
|
442
|
+
|
|
443
|
+
=== CRITICAL: READ-ONLY MODE — NO FILE MODIFICATIONS ===
|
|
444
|
+
You may only read and search. No edit/write/delete/move operations.
|
|
445
|
+
|
|
446
|
+
Authoritative sources, in priority order:
|
|
447
|
+
1. `AGENTS.md` at the project root — architectural reference
|
|
448
|
+
2. `docs/*.md` — user-facing documentation
|
|
449
|
+
3. `aru.json` examples in the codebase — config shape
|
|
450
|
+
4. Reading the code under `aru/` directly (last resort — prefer docs)
|
|
451
|
+
|
|
452
|
+
Workflow:
|
|
453
|
+
1. `read_file` AGENTS.md first
|
|
454
|
+
2. `glob_search` + `read_file` relevant docs/*.md
|
|
455
|
+
3. Search `aru.json` or permission config examples if the question is
|
|
456
|
+
configuration-related
|
|
457
|
+
|
|
458
|
+
Never invent features. If the docs do not cover the topic, say so and
|
|
459
|
+
suggest the closest available alternative. Cite file paths in your
|
|
460
|
+
response so the user can verify.
|
|
461
|
+
|
|
462
|
+
Return ONE final message.\
|
|
463
|
+
"""
|
|
464
|
+
|
|
465
|
+
|
|
377
466
|
def build_instructions(role: str, extra: str = "") -> str:
|
|
378
467
|
"""Build complete instructions for an agent role.
|
|
379
468
|
|
|
380
469
|
Args:
|
|
381
|
-
role: One of 'planner', 'executor', 'general', 'explorer'
|
|
470
|
+
role: One of 'planner', 'executor', 'general', 'explorer', 'verifier',
|
|
471
|
+
'reviewer', 'guide'.
|
|
382
472
|
extra: Additional project-specific instructions (README, AGENTS.md, skills).
|
|
383
473
|
"""
|
|
384
474
|
role_text = {
|
|
@@ -386,6 +476,9 @@ def build_instructions(role: str, extra: str = "") -> str:
|
|
|
386
476
|
"executor": EXECUTOR_ROLE,
|
|
387
477
|
"general": GENERAL_ROLE,
|
|
388
478
|
"explorer": EXPLORER_ROLE,
|
|
479
|
+
"verifier": VERIFIER_ROLE,
|
|
480
|
+
"reviewer": REVIEWER_ROLE,
|
|
481
|
+
"guide": GUIDE_ROLE,
|
|
389
482
|
}[role]
|
|
390
483
|
|
|
391
484
|
parts = [role_text, BASE_INSTRUCTIONS]
|
|
@@ -26,6 +26,15 @@ class AgentSpec:
|
|
|
26
26
|
An explicit int caps the agent below that ceiling — providers.py always
|
|
27
27
|
clamps the final value to min(requested, model_cap) so specs can never
|
|
28
28
|
ask for more than the model supports.
|
|
29
|
+
|
|
30
|
+
`description` is the LLM-facing summary rendered into `delegate_task`'s
|
|
31
|
+
docstring. Only subagent specs need a meaningful description (primary
|
|
32
|
+
agents are never picked via `agent_name`). Keep it short (1-3 sentences)
|
|
33
|
+
and directive — the model uses it to decide when this agent fits.
|
|
34
|
+
|
|
35
|
+
`extra_instructions` is appended to the base role instructions when the
|
|
36
|
+
agent is built. Use it for agent-specific policy ("you are read-only,
|
|
37
|
+
never call write tools") that shouldn't leak into other roles.
|
|
29
38
|
"""
|
|
30
39
|
|
|
31
40
|
name: str # display name passed to Agno
|
|
@@ -35,6 +44,8 @@ class AgentSpec:
|
|
|
35
44
|
max_tokens: int | None
|
|
36
45
|
small_model: bool = False # if True, factory uses ctx.small_model_ref
|
|
37
46
|
use_reasoning: bool = True # False skips thinking params (e.g. explorer)
|
|
47
|
+
description: str = "" # LLM-facing summary for `delegate_task` docstring
|
|
48
|
+
extra_instructions: str = "" # appended to base role instructions on build
|
|
38
49
|
|
|
39
50
|
|
|
40
51
|
def _build_tools() -> list:
|
|
@@ -90,5 +101,57 @@ AGENTS: dict[str, AgentSpec] = {
|
|
|
90
101
|
max_tokens=8192,
|
|
91
102
|
small_model=True,
|
|
92
103
|
use_reasoning=False, # fast read-only subagent — no thinking overhead
|
|
104
|
+
description=(
|
|
105
|
+
"Fast read-only codebase exploration agent. Use for searching "
|
|
106
|
+
"files, finding patterns, reading code, and understanding "
|
|
107
|
+
"structure. Specify thoroughness in the task text: \"quick\" "
|
|
108
|
+
"(basic searches), \"medium\" (moderate exploration), or "
|
|
109
|
+
"\"very thorough\" (comprehensive analysis)."
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
"verification": AgentSpec(
|
|
113
|
+
name="Verifier",
|
|
114
|
+
role="verifier",
|
|
115
|
+
mode="subagent",
|
|
116
|
+
tools_factory=_explore_tools, # read-only
|
|
117
|
+
max_tokens=4096,
|
|
118
|
+
small_model=True,
|
|
119
|
+
use_reasoning=False,
|
|
120
|
+
description=(
|
|
121
|
+
"Double-check a recent batch of edits for correctness. Reads "
|
|
122
|
+
"changed files, searches for call sites, reports inconsistencies "
|
|
123
|
+
"and missing follow-up edits. Read-only — never edits. Use after "
|
|
124
|
+
"non-trivial multi-file edits to catch issues before the user sees them."
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
"reviewer": AgentSpec(
|
|
128
|
+
name="Reviewer",
|
|
129
|
+
role="reviewer",
|
|
130
|
+
mode="subagent",
|
|
131
|
+
tools_factory=_explore_tools, # read-only
|
|
132
|
+
max_tokens=4096,
|
|
133
|
+
small_model=True,
|
|
134
|
+
use_reasoning=False,
|
|
135
|
+
description=(
|
|
136
|
+
"Code review against naming, error handling, test coverage, and "
|
|
137
|
+
"security heuristics. Read-only; produces bulleted findings with "
|
|
138
|
+
"file:line refs and severity tags. Use when you want a second "
|
|
139
|
+
"pair of eyes before finalising changes."
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
"guide": AgentSpec(
|
|
143
|
+
name="Guide",
|
|
144
|
+
role="guide",
|
|
145
|
+
mode="subagent",
|
|
146
|
+
tools_factory=_explore_tools, # read-only
|
|
147
|
+
max_tokens=4096,
|
|
148
|
+
small_model=True,
|
|
149
|
+
use_reasoning=False,
|
|
150
|
+
description=(
|
|
151
|
+
"Answer questions about using Aru itself — slash commands, "
|
|
152
|
+
"permission config, skills, plugins, tool catalog. Reads "
|
|
153
|
+
"AGENTS.md and docs/ to ground answers. Use when the user's "
|
|
154
|
+
"question is about Aru's features, not their own codebase."
|
|
155
|
+
),
|
|
93
156
|
),
|
|
94
157
|
}
|
|
@@ -634,6 +634,25 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
634
634
|
))
|
|
635
635
|
continue
|
|
636
636
|
|
|
637
|
+
if user_input.lower() == "/subagents":
|
|
638
|
+
from aru.commands import handle_subagents_command
|
|
639
|
+
handle_subagents_command(session)
|
|
640
|
+
continue
|
|
641
|
+
|
|
642
|
+
if user_input.lower().startswith("/subagent "):
|
|
643
|
+
from aru.commands import handle_subagent_detail_command
|
|
644
|
+
handle_subagent_detail_command(session, user_input[10:].strip())
|
|
645
|
+
continue
|
|
646
|
+
if user_input.lower() == "/subagent":
|
|
647
|
+
from aru.commands import handle_subagent_detail_command
|
|
648
|
+
handle_subagent_detail_command(session, "")
|
|
649
|
+
continue
|
|
650
|
+
|
|
651
|
+
if user_input.lower() == "/bg":
|
|
652
|
+
from aru.commands import handle_background_command
|
|
653
|
+
handle_background_command(session)
|
|
654
|
+
continue
|
|
655
|
+
|
|
637
656
|
if user_input.lower() in ("/yolo", "/unsafe"):
|
|
638
657
|
_toggle_yolo_mode(ctx)
|
|
639
658
|
continue
|
|
@@ -776,6 +795,16 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
776
795
|
console.print(f"[dim]Agents: {', '.join(f'/{k}' for k in primary)}[/dim]")
|
|
777
796
|
|
|
778
797
|
else:
|
|
798
|
+
# Drain any background sub-agent notifications that completed
|
|
799
|
+
# during the previous turn. Prepend them to the user prompt so
|
|
800
|
+
# the model sees the results before processing the new message.
|
|
801
|
+
# Parity with claude-code's <task-notification> routing.
|
|
802
|
+
from aru.tools.delegate import drain_pending_notifications
|
|
803
|
+
bg_notifications = drain_pending_notifications(session)
|
|
804
|
+
effective_prompt = user_input
|
|
805
|
+
if bg_notifications:
|
|
806
|
+
effective_prompt = f"{bg_notifications}\n\n{user_input}"
|
|
807
|
+
|
|
779
808
|
# Check for @agent mention anywhere in message
|
|
780
809
|
agent_mention = _extract_agent_mention(user_input, config.custom_agents)
|
|
781
810
|
if agent_mention:
|
|
@@ -785,12 +814,14 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
785
814
|
console.print(f"[bold magenta]Routing to @{agent_name}...[/bold magenta]")
|
|
786
815
|
agent = await create_custom_agent_instance(agent_def, session, config, env_context=_build_env_ctx())
|
|
787
816
|
session.add_message("user", user_input)
|
|
817
|
+
if bg_notifications:
|
|
818
|
+
message_text = f"{bg_notifications}\n\n{message_text}"
|
|
788
819
|
with permission_scope(agent_def.permission):
|
|
789
820
|
await run_agent_capture(agent, message_text, session, images=attached_images or None)
|
|
790
821
|
else:
|
|
791
822
|
agent = await create_general_agent(session, config, env_context=_build_env_ctx())
|
|
792
823
|
session.add_message("user", user_input)
|
|
793
|
-
await run_agent_capture(agent,
|
|
824
|
+
await run_agent_capture(agent, effective_prompt, session, images=attached_images or None)
|
|
794
825
|
|
|
795
826
|
# Show token usage and auto-save
|
|
796
827
|
if session.token_summary:
|
|
@@ -21,6 +21,9 @@ SLASH_COMMANDS = [
|
|
|
21
21
|
("/commands", "List custom commands", "/commands"),
|
|
22
22
|
("/skills", "List available skills", "/skills"),
|
|
23
23
|
("/agents", "List custom agents", "/agents"),
|
|
24
|
+
("/subagents", "Show sub-agent invocation tree for this session", "/subagents"),
|
|
25
|
+
("/subagent", "Show detailed trace for a sub-agent by task_id", "/subagent <task_id>"),
|
|
26
|
+
("/bg", "List background sub-agent tasks (pending notifications)", "/bg"),
|
|
24
27
|
("/mcp", "List loaded MCP tools", "/mcp"),
|
|
25
28
|
("/plugin", "Manage cached plugins (install/list/remove/update)", "/plugin <subcommand>"),
|
|
26
29
|
("/undo", "Undo last turn — restore files and/or conversation", "/undo"),
|
|
@@ -72,6 +75,135 @@ def ask_yes_no(prompt: str) -> bool:
|
|
|
72
75
|
return False
|
|
73
76
|
|
|
74
77
|
|
|
78
|
+
def handle_subagents_command(session) -> None:
|
|
79
|
+
"""Render the session's sub-agent trace tree (`/subagents`).
|
|
80
|
+
|
|
81
|
+
Flat tabular output — task_id, agent name, duration, tokens in/out,
|
|
82
|
+
status, task preview. Nested delegations indent under their parent.
|
|
83
|
+
"""
|
|
84
|
+
from rich.table import Table
|
|
85
|
+
|
|
86
|
+
traces = list(getattr(session, "subagent_traces", []) or [])
|
|
87
|
+
if not traces:
|
|
88
|
+
console.print("[dim]No sub-agents invoked in this session.[/dim]")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
children_of: dict[str | None, list] = {}
|
|
92
|
+
for t in traces:
|
|
93
|
+
children_of.setdefault(t.parent_id, []).append(t)
|
|
94
|
+
|
|
95
|
+
status_style = {
|
|
96
|
+
"running": "yellow",
|
|
97
|
+
"completed": "green",
|
|
98
|
+
"cancelled": "dim",
|
|
99
|
+
"error": "red",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
table = Table(show_header=True, header_style="bold")
|
|
103
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
104
|
+
table.add_column("Agent", no_wrap=True)
|
|
105
|
+
table.add_column("Duration", justify="right", no_wrap=True)
|
|
106
|
+
table.add_column("Tokens (in/out)", justify="right", no_wrap=True)
|
|
107
|
+
table.add_column("Status", no_wrap=True)
|
|
108
|
+
table.add_column("Task")
|
|
109
|
+
|
|
110
|
+
def _emit(node, depth: int = 0):
|
|
111
|
+
indent = " " * depth + ("└ " if depth else "")
|
|
112
|
+
task_id = node.task_id[:12]
|
|
113
|
+
dur = f"{node.duration:.1f}s" if node.ended_at else "…"
|
|
114
|
+
tokens = f"{node.tokens_in:,}/{node.tokens_out:,}"
|
|
115
|
+
status = f"[{status_style.get(node.status, 'white')}]{node.status}[/]"
|
|
116
|
+
task_preview = (node.task[:60] + "…") if len(node.task) > 60 else node.task
|
|
117
|
+
table.add_row(
|
|
118
|
+
f"{indent}{task_id}", node.agent_name, dur, tokens, status, task_preview
|
|
119
|
+
)
|
|
120
|
+
for child in children_of.get(node.task_id, []):
|
|
121
|
+
_emit(child, depth + 1)
|
|
122
|
+
|
|
123
|
+
trace_ids = {t.task_id for t in traces}
|
|
124
|
+
roots = [t for t in traces if t.parent_id not in trace_ids]
|
|
125
|
+
for root in roots:
|
|
126
|
+
_emit(root)
|
|
127
|
+
|
|
128
|
+
console.print(
|
|
129
|
+
Panel(table, title=f"Sub-agent invocations ({len(traces)})",
|
|
130
|
+
border_style="cyan", padding=(0, 1))
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def handle_subagent_detail_command(session, task_id: str) -> None:
|
|
135
|
+
"""Print detailed trace for one sub-agent by task_id prefix."""
|
|
136
|
+
task_id = task_id.strip()
|
|
137
|
+
if not task_id:
|
|
138
|
+
console.print("[yellow]Usage: /subagent <task_id>[/yellow]")
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
traces = list(getattr(session, "subagent_traces", []) or [])
|
|
142
|
+
matches = [t for t in traces if t.task_id == task_id or t.task_id.startswith(task_id)]
|
|
143
|
+
if not matches:
|
|
144
|
+
console.print(f"[yellow]No sub-agent found with task_id starting with '{task_id}'[/yellow]")
|
|
145
|
+
return
|
|
146
|
+
if len(matches) > 1:
|
|
147
|
+
console.print(
|
|
148
|
+
f"[yellow]Ambiguous: {len(matches)} traces match '{task_id}'. Showing the first.[/yellow]"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
trace = matches[0]
|
|
152
|
+
lines: list[str] = [
|
|
153
|
+
f"[bold]task_id:[/bold] {trace.task_id}",
|
|
154
|
+
f"[bold]agent:[/bold] {trace.agent_name}",
|
|
155
|
+
f"[bold]status:[/bold] {trace.status}",
|
|
156
|
+
f"[bold]parent:[/bold] {trace.parent_id or '(primary)'}",
|
|
157
|
+
]
|
|
158
|
+
if trace.ended_at:
|
|
159
|
+
lines.append(f"[bold]duration:[/bold] {trace.duration:.2f}s")
|
|
160
|
+
else:
|
|
161
|
+
lines.append("[bold]duration:[/bold] (running)")
|
|
162
|
+
lines.extend([
|
|
163
|
+
f"[bold]tokens:[/bold] in={trace.tokens_in:,} out={trace.tokens_out:,}",
|
|
164
|
+
"",
|
|
165
|
+
"[bold]task:[/bold]",
|
|
166
|
+
f" {trace.task}",
|
|
167
|
+
"",
|
|
168
|
+
])
|
|
169
|
+
if trace.tool_calls:
|
|
170
|
+
lines.append(f"[bold]tool calls ({len(trace.tool_calls)}):[/bold]")
|
|
171
|
+
for i, call in enumerate(trace.tool_calls, 1):
|
|
172
|
+
args = call.get("args_preview", "")
|
|
173
|
+
dur = call.get("duration", 0)
|
|
174
|
+
lines.append(f" {i}. [cyan]{call.get('tool', '?')}[/cyan] ({dur}s) {args}")
|
|
175
|
+
lines.append("")
|
|
176
|
+
if trace.result:
|
|
177
|
+
lines.append("[bold]result preview:[/bold]")
|
|
178
|
+
lines.append(" " + trace.result.replace("\n", "\n "))
|
|
179
|
+
|
|
180
|
+
console.print(Panel("\n".join(lines), title="Sub-agent trace", border_style="cyan", padding=(1, 2)))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def handle_background_command(session) -> None:
|
|
184
|
+
"""List pending background-task notifications (`/bg`).
|
|
185
|
+
|
|
186
|
+
Each entry is a sub-agent spawned with `run_in_background=True` that
|
|
187
|
+
has already completed but hasn't been surfaced to the primary agent
|
|
188
|
+
yet. Notifications are drained automatically on the next turn — this
|
|
189
|
+
command just lets the user see what's queued.
|
|
190
|
+
"""
|
|
191
|
+
pending = list(getattr(session, "pending_notifications", []) or [])
|
|
192
|
+
if not pending:
|
|
193
|
+
console.print("[dim]No pending background tasks.[/dim]")
|
|
194
|
+
return
|
|
195
|
+
for n in pending:
|
|
196
|
+
tid = n.get("task_id", "?")
|
|
197
|
+
result = n.get("result", "")
|
|
198
|
+
preview = (result[:200] + "…") if len(result) > 200 else result
|
|
199
|
+
console.print(Panel(
|
|
200
|
+
preview,
|
|
201
|
+
title=f"[bold]Background task: {tid}[/bold]",
|
|
202
|
+
border_style="cyan",
|
|
203
|
+
padding=(0, 1),
|
|
204
|
+
))
|
|
205
|
+
|
|
206
|
+
|
|
75
207
|
def handle_plugin_command(args: str) -> None:
|
|
76
208
|
"""Handle /plugin <subcommand> [args] — install/list/remove/update/info."""
|
|
77
209
|
from rich.table import Table
|