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.
Files changed (98) hide show
  1. {aru_code-0.32.0 → aru_code-0.33.0}/PKG-INFO +1 -1
  2. aru_code-0.33.0/aru/__init__.py +1 -0
  3. {aru_code-0.32.0 → aru_code-0.33.0}/aru/agent_factory.py +9 -1
  4. {aru_code-0.32.0 → aru_code-0.33.0}/aru/agents/base.py +94 -1
  5. {aru_code-0.32.0 → aru_code-0.33.0}/aru/agents/catalog.py +63 -0
  6. {aru_code-0.32.0 → aru_code-0.33.0}/aru/cli.py +32 -1
  7. {aru_code-0.32.0 → aru_code-0.33.0}/aru/commands.py +132 -0
  8. {aru_code-0.32.0 → aru_code-0.33.0}/aru/permissions.py +318 -21
  9. {aru_code-0.32.0 → aru_code-0.33.0}/aru/runtime.py +78 -1
  10. {aru_code-0.32.0 → aru_code-0.33.0}/aru/session.py +87 -0
  11. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tool_policy.py +75 -49
  12. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/codebase.py +1 -1
  13. aru_code-0.33.0/aru/tools/delegate.py +602 -0
  14. aru_code-0.33.0/aru/tools/delegate_prompt.txt +34 -0
  15. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/file_ops.py +2 -2
  16. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/registry.py +10 -5
  17. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/skill.py +1 -1
  18. {aru_code-0.32.0 → aru_code-0.33.0}/aru_code.egg-info/PKG-INFO +1 -1
  19. {aru_code-0.32.0 → aru_code-0.33.0}/aru_code.egg-info/SOURCES.txt +2 -0
  20. {aru_code-0.32.0 → aru_code-0.33.0}/pyproject.toml +4 -1
  21. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_catalog.py +8 -1
  22. aru_code-0.33.0/tests/test_delegate.py +1063 -0
  23. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_invoke_skill.py +4 -4
  24. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_permissions.py +501 -0
  25. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_tool_policy.py +88 -0
  26. aru_code-0.32.0/aru/__init__.py +0 -1
  27. aru_code-0.32.0/aru/tools/delegate.py +0 -236
  28. {aru_code-0.32.0 → aru_code-0.33.0}/LICENSE +0 -0
  29. {aru_code-0.32.0 → aru_code-0.33.0}/README.md +0 -0
  30. {aru_code-0.32.0 → aru_code-0.33.0}/aru/agents/__init__.py +0 -0
  31. {aru_code-0.32.0 → aru_code-0.33.0}/aru/agents/planner.py +0 -0
  32. {aru_code-0.32.0 → aru_code-0.33.0}/aru/cache_patch.py +0 -0
  33. {aru_code-0.32.0 → aru_code-0.33.0}/aru/checkpoints.py +0 -0
  34. {aru_code-0.32.0 → aru_code-0.33.0}/aru/completers.py +0 -0
  35. {aru_code-0.32.0 → aru_code-0.33.0}/aru/config.py +0 -0
  36. {aru_code-0.32.0 → aru_code-0.33.0}/aru/context.py +0 -0
  37. {aru_code-0.32.0 → aru_code-0.33.0}/aru/display.py +0 -0
  38. {aru_code-0.32.0 → aru_code-0.33.0}/aru/history_blocks.py +0 -0
  39. {aru_code-0.32.0 → aru_code-0.33.0}/aru/plugin_cache.py +0 -0
  40. {aru_code-0.32.0 → aru_code-0.33.0}/aru/plugins/__init__.py +0 -0
  41. {aru_code-0.32.0 → aru_code-0.33.0}/aru/plugins/custom_tools.py +0 -0
  42. {aru_code-0.32.0 → aru_code-0.33.0}/aru/plugins/hooks.py +0 -0
  43. {aru_code-0.32.0 → aru_code-0.33.0}/aru/plugins/manager.py +0 -0
  44. {aru_code-0.32.0 → aru_code-0.33.0}/aru/plugins/tool_api.py +0 -0
  45. {aru_code-0.32.0 → aru_code-0.33.0}/aru/providers.py +0 -0
  46. {aru_code-0.32.0 → aru_code-0.33.0}/aru/runner.py +0 -0
  47. {aru_code-0.32.0 → aru_code-0.33.0}/aru/select.py +0 -0
  48. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/__init__.py +0 -0
  49. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/_diff.py +0 -0
  50. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/_shared.py +0 -0
  51. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/ast_tools.py +0 -0
  52. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/gitignore.py +0 -0
  53. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/mcp_client.py +0 -0
  54. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/plan_mode.py +0 -0
  55. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/ranker.py +0 -0
  56. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/search.py +0 -0
  57. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/shell.py +0 -0
  58. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/tasklist.py +0 -0
  59. {aru_code-0.32.0 → aru_code-0.33.0}/aru/tools/web.py +0 -0
  60. {aru_code-0.32.0 → aru_code-0.33.0}/aru_code.egg-info/dependency_links.txt +0 -0
  61. {aru_code-0.32.0 → aru_code-0.33.0}/aru_code.egg-info/entry_points.txt +0 -0
  62. {aru_code-0.32.0 → aru_code-0.33.0}/aru_code.egg-info/requires.txt +0 -0
  63. {aru_code-0.32.0 → aru_code-0.33.0}/aru_code.egg-info/top_level.txt +0 -0
  64. {aru_code-0.32.0 → aru_code-0.33.0}/setup.cfg +0 -0
  65. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_agents_base.py +0 -0
  66. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_agents_md_coverage.py +0 -0
  67. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cache_patch_metrics.py +0 -0
  68. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cache_patch_stop_reason.py +0 -0
  69. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_checkpoints.py +0 -0
  70. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli.py +0 -0
  71. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_advanced.py +0 -0
  72. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_base.py +0 -0
  73. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_completers.py +0 -0
  74. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_new.py +0 -0
  75. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_run_cli.py +0 -0
  76. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_session.py +0 -0
  77. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_cli_shell.py +0 -0
  78. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_codebase.py +0 -0
  79. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_confabulation_regression.py +0 -0
  80. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_config.py +0 -0
  81. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_context.py +0 -0
  82. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_gitignore.py +0 -0
  83. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_guardrails_scenarios.py +0 -0
  84. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_invoked_skills.py +0 -0
  85. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_main.py +0 -0
  86. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_mcp_client.py +0 -0
  87. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_microcompact.py +0 -0
  88. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_plan_mode_refactor.py +0 -0
  89. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_plugin_cache.py +0 -0
  90. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_plugins.py +0 -0
  91. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_providers.py +0 -0
  92. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_ranker.py +0 -0
  93. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_reasoning.py +0 -0
  94. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_runner_recovery.py +0 -0
  95. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_runtime.py +0 -0
  96. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_select.py +0 -0
  97. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_skill_disallowed_tools.py +0 -0
  98. {aru_code-0.32.0 → aru_code-0.33.0}/tests/test_tasklist.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.32.0
3
+ Version: 0.33.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.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 = _build_instructions(spec.role, extra_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, user_input, session, images=attached_images or None)
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