aru-code 0.7.0__tar.gz → 0.8.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.7.0/aru_code.egg-info → aru_code-0.8.0}/PKG-INFO +1 -1
- aru_code-0.8.0/aru/__init__.py +1 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/agent_factory.py +6 -4
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/agents/executor.py +3 -2
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/agents/planner.py +3 -3
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/cli.py +13 -19
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/context.py +2 -2
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/permissions.py +39 -73
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/runner.py +12 -21
- aru_code-0.8.0/aru/runtime.py +158 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/tools/codebase.py +140 -195
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/tools/tasklist.py +20 -75
- {aru_code-0.7.0 → aru_code-0.8.0/aru_code.egg-info}/PKG-INFO +1 -1
- {aru_code-0.7.0 → aru_code-0.8.0}/aru_code.egg-info/SOURCES.txt +1 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/pyproject.toml +1 -1
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_codebase.py +16 -16
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_permissions.py +26 -20
- aru_code-0.7.0/aru/__init__.py +0 -1
- {aru_code-0.7.0 → aru_code-0.8.0}/LICENSE +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/README.md +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/agents/base.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/commands.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/completers.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/config.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/display.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/providers.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/session.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/setup.cfg +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_ast_tools.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_cli.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_config.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_context.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_executor.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_main.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_planner.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_providers.py +0 -0
- {aru_code-0.7.0 → aru_code-0.8.0}/tests/test_ranker.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.0"
|
|
@@ -13,7 +13,8 @@ def create_general_agent(session: Session, config: AgentConfig | None = None):
|
|
|
13
13
|
from agno.agent import Agent
|
|
14
14
|
from agno.compression.manager import CompressionManager
|
|
15
15
|
|
|
16
|
-
from aru.tools.codebase import GENERAL_TOOLS
|
|
16
|
+
from aru.tools.codebase import GENERAL_TOOLS
|
|
17
|
+
from aru.runtime import get_ctx
|
|
17
18
|
|
|
18
19
|
extra = config.get_extra_instructions() if config else ""
|
|
19
20
|
|
|
@@ -25,7 +26,7 @@ def create_general_agent(session: Session, config: AgentConfig | None = None):
|
|
|
25
26
|
markdown=True,
|
|
26
27
|
compress_tool_results=True,
|
|
27
28
|
compression_manager=CompressionManager(
|
|
28
|
-
model=create_model(
|
|
29
|
+
model=create_model(get_ctx().small_model_ref, max_tokens=1024),
|
|
29
30
|
compress_tool_results=True,
|
|
30
31
|
compress_tool_results_limit=15,
|
|
31
32
|
),
|
|
@@ -39,7 +40,8 @@ def create_custom_agent_instance(agent_def: CustomAgent, session: Session,
|
|
|
39
40
|
from agno.agent import Agent
|
|
40
41
|
from agno.compression.manager import CompressionManager
|
|
41
42
|
from aru.agents.base import BASE_INSTRUCTIONS
|
|
42
|
-
from aru.tools.codebase import resolve_tools
|
|
43
|
+
from aru.tools.codebase import resolve_tools
|
|
44
|
+
from aru.runtime import get_ctx
|
|
43
45
|
|
|
44
46
|
model_ref = agent_def.model or session.model_ref
|
|
45
47
|
tools = resolve_tools(agent_def.tools)
|
|
@@ -58,7 +60,7 @@ def create_custom_agent_instance(agent_def: CustomAgent, session: Session,
|
|
|
58
60
|
markdown=True,
|
|
59
61
|
compress_tool_results=True,
|
|
60
62
|
compression_manager=CompressionManager(
|
|
61
|
-
model=create_model(
|
|
63
|
+
model=create_model(get_ctx().small_model_ref, max_tokens=1024),
|
|
62
64
|
compress_tool_results=True,
|
|
63
65
|
compress_tool_results_limit=15,
|
|
64
66
|
),
|
|
@@ -6,7 +6,8 @@ from agno.utils.log import log_warning
|
|
|
6
6
|
|
|
7
7
|
from aru.agents.base import build_instructions
|
|
8
8
|
from aru.providers import create_model
|
|
9
|
-
from aru.tools.codebase import EXECUTOR_TOOLS
|
|
9
|
+
from aru.tools.codebase import EXECUTOR_TOOLS
|
|
10
|
+
from aru.runtime import get_ctx
|
|
10
11
|
|
|
11
12
|
# Max chars for truncation fallback when compression fails
|
|
12
13
|
_TRUNCATE_FALLBACK = 3000
|
|
@@ -50,7 +51,7 @@ def create_executor(model_ref: str = "anthropic/claude-sonnet-4-5", extra_instru
|
|
|
50
51
|
# Compress tool results after 5 uncompressed tool calls to save tokens
|
|
51
52
|
compress_tool_results=True,
|
|
52
53
|
compression_manager=_SafeCompressionManager(
|
|
53
|
-
model=create_model(
|
|
54
|
+
model=create_model(get_ctx().small_model_ref, max_tokens=2048),
|
|
54
55
|
compress_tool_results=True,
|
|
55
56
|
compress_tool_results_limit=15,
|
|
56
57
|
),
|
|
@@ -6,9 +6,9 @@ from agno.compression.manager import CompressionManager
|
|
|
6
6
|
from aru.agents.base import build_instructions
|
|
7
7
|
from aru.providers import create_model
|
|
8
8
|
from aru.tools.codebase import (
|
|
9
|
-
_get_small_model_ref,
|
|
10
9
|
glob_search, grep_search, list_directory, read_file, read_file_smart,
|
|
11
10
|
)
|
|
11
|
+
from aru.runtime import get_ctx
|
|
12
12
|
|
|
13
13
|
REVIEWER_INSTRUCTIONS = """\
|
|
14
14
|
You are a plan scope reviewer. You receive a user request and a generated implementation plan.
|
|
@@ -47,7 +47,7 @@ async def review_plan(request: str, plan: str) -> str:
|
|
|
47
47
|
"""
|
|
48
48
|
reviewer = Agent(
|
|
49
49
|
name="Reviewer",
|
|
50
|
-
model=create_model(
|
|
50
|
+
model=create_model(get_ctx().small_model_ref, max_tokens=2048),
|
|
51
51
|
instructions=REVIEWER_INSTRUCTIONS,
|
|
52
52
|
markdown=True,
|
|
53
53
|
)
|
|
@@ -77,7 +77,7 @@ def create_planner(model_ref: str = "anthropic/claude-sonnet-4-5", extra_instruc
|
|
|
77
77
|
# Compress tool results after 6 uncompressed tool calls to save tokens
|
|
78
78
|
compress_tool_results=True,
|
|
79
79
|
compression_manager=CompressionManager(
|
|
80
|
-
model=create_model(
|
|
80
|
+
model=create_model(get_ctx().small_model_ref, max_tokens=1024),
|
|
81
81
|
compress_tool_results=True,
|
|
82
82
|
compress_tool_results_limit=15,
|
|
83
83
|
),
|
|
@@ -104,24 +104,18 @@ from aru.providers import (
|
|
|
104
104
|
|
|
105
105
|
async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
106
106
|
"""Main REPL loop."""
|
|
107
|
-
|
|
108
|
-
from aru.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
parse_permission_config,
|
|
114
|
-
)
|
|
115
|
-
from aru.tools.codebase import set_console
|
|
116
|
-
set_console(console)
|
|
117
|
-
perm_set_console(console)
|
|
118
|
-
set_skip_permissions(skip_permissions)
|
|
107
|
+
import atexit
|
|
108
|
+
from aru.runtime import init_ctx, get_ctx
|
|
109
|
+
from aru.permissions import parse_permission_config, reset_session as perm_reset_session
|
|
110
|
+
from aru.tools.codebase import cleanup_processes
|
|
111
|
+
|
|
112
|
+
ctx = init_ctx(console=console, skip_permissions=skip_permissions)
|
|
119
113
|
|
|
120
114
|
store = SessionStore()
|
|
121
115
|
|
|
122
116
|
def _sync_model(sess: Session):
|
|
123
|
-
"""Sync the model IDs to the
|
|
124
|
-
|
|
117
|
+
"""Sync the model IDs to the RuntimeContext from the session's model_ref."""
|
|
118
|
+
ctx.model_id = sess.model_id
|
|
125
119
|
small_ref = config.model_aliases.get("small") if config else None
|
|
126
120
|
if not small_ref:
|
|
127
121
|
provider_key, _ = resolve_model_ref(sess.model_ref)
|
|
@@ -133,7 +127,7 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
133
127
|
"ollama": "ollama/llama3.1",
|
|
134
128
|
}
|
|
135
129
|
small_ref = _small_defaults.get(provider_key, sess.model_ref)
|
|
136
|
-
|
|
130
|
+
ctx.small_model_ref = small_ref
|
|
137
131
|
|
|
138
132
|
# Load project configuration
|
|
139
133
|
config = load_config()
|
|
@@ -155,8 +149,7 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
155
149
|
from aru.tools.codebase import set_custom_agents
|
|
156
150
|
set_custom_agents(config.custom_agents)
|
|
157
151
|
if config.permissions:
|
|
158
|
-
perm_config = parse_permission_config(config.permissions)
|
|
159
|
-
set_perm_config(perm_config)
|
|
152
|
+
ctx.perm_config = parse_permission_config(config.permissions)
|
|
160
153
|
console.print("[dim]Loaded permission config[/dim]")
|
|
161
154
|
|
|
162
155
|
extra_instructions = config.get_extra_instructions()
|
|
@@ -188,8 +181,9 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
188
181
|
_sync_model(session)
|
|
189
182
|
_render_home(session, skip_permissions)
|
|
190
183
|
|
|
191
|
-
# Wire file-mutation callback
|
|
192
|
-
|
|
184
|
+
# Wire file-mutation callback and atexit cleanup
|
|
185
|
+
ctx.on_file_mutation = session.invalidate_context_cache
|
|
186
|
+
atexit.register(lambda: cleanup_processes(ctx.tracked_processes))
|
|
193
187
|
|
|
194
188
|
planner = None
|
|
195
189
|
executor = None
|
|
@@ -204,7 +204,7 @@ async def compact_conversation(
|
|
|
204
204
|
Uses a small/fast model for the summarization to minimize cost.
|
|
205
205
|
Falls back to simple truncation if the agent call fails.
|
|
206
206
|
"""
|
|
207
|
-
from aru.
|
|
207
|
+
from aru.runtime import get_ctx
|
|
208
208
|
from aru.providers import create_model
|
|
209
209
|
|
|
210
210
|
prompt = build_compaction_prompt(history, plan_task)
|
|
@@ -212,7 +212,7 @@ async def compact_conversation(
|
|
|
212
212
|
try:
|
|
213
213
|
from agno.agent import Agent
|
|
214
214
|
|
|
215
|
-
small_ref =
|
|
215
|
+
small_ref = get_ctx().small_model_ref
|
|
216
216
|
compactor = Agent(
|
|
217
217
|
name="Compactor",
|
|
218
218
|
model=create_model(small_ref, max_tokens=2048),
|
|
@@ -19,15 +19,16 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import fnmatch
|
|
21
21
|
import os
|
|
22
|
-
import threading
|
|
23
22
|
from contextlib import contextmanager
|
|
24
23
|
from dataclasses import dataclass, field
|
|
25
24
|
from typing import Any, Generator, Literal
|
|
26
25
|
|
|
27
|
-
from rich.console import
|
|
26
|
+
from rich.console import Group
|
|
28
27
|
from rich.panel import Panel
|
|
29
28
|
from rich.text import Text
|
|
30
29
|
|
|
30
|
+
from aru.runtime import get_ctx
|
|
31
|
+
|
|
31
32
|
PermissionAction = Literal["allow", "ask", "deny"]
|
|
32
33
|
|
|
33
34
|
VALID_ACTIONS: set[str] = {"allow", "ask", "deny"}
|
|
@@ -96,62 +97,24 @@ for _prefix in SAFE_COMMAND_PREFIXES:
|
|
|
96
97
|
|
|
97
98
|
|
|
98
99
|
# ---------------------------------------------------------------------------
|
|
99
|
-
#
|
|
100
|
+
# Thin wrappers over RuntimeContext (preserve public API for callers)
|
|
100
101
|
# ---------------------------------------------------------------------------
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
_skip_permissions: bool = False
|
|
105
|
-
_permission_lock = threading.Lock()
|
|
106
|
-
_live = None
|
|
107
|
-
_display = None
|
|
108
|
-
_console = Console()
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
# ---------------------------------------------------------------------------
|
|
112
|
-
# Setters
|
|
113
|
-
# ---------------------------------------------------------------------------
|
|
103
|
+
def set_config(config: PermissionConfig) -> None:
|
|
104
|
+
get_ctx().perm_config = config
|
|
114
105
|
|
|
115
|
-
def set_config(config: PermissionConfig):
|
|
116
|
-
global _config
|
|
117
|
-
_config = config
|
|
118
106
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
global _skip_permissions
|
|
122
|
-
_skip_permissions = value
|
|
107
|
+
def set_skip_permissions(value: bool) -> None:
|
|
108
|
+
get_ctx().skip_permissions = value
|
|
123
109
|
|
|
124
110
|
|
|
125
111
|
def get_skip_permissions() -> bool:
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def set_live(live):
|
|
130
|
-
global _live
|
|
131
|
-
_live = live
|
|
132
|
-
|
|
112
|
+
return get_ctx().skip_permissions
|
|
133
113
|
|
|
134
|
-
def set_display(display):
|
|
135
|
-
global _display
|
|
136
|
-
_display = display
|
|
137
114
|
|
|
138
|
-
|
|
139
|
-
def set_console(console: Console):
|
|
140
|
-
global _console
|
|
141
|
-
_console = console
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def reset_session():
|
|
115
|
+
def reset_session() -> None:
|
|
145
116
|
"""Reset session-level permission state (call between conversations)."""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
# ---------------------------------------------------------------------------
|
|
150
|
-
# Agent-level permission scoping
|
|
151
|
-
# ---------------------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
_config_stack: list[PermissionConfig] = []
|
|
154
|
-
_session_stack: list[set[tuple[str, str]]] = []
|
|
117
|
+
get_ctx().session_allowed.clear()
|
|
155
118
|
|
|
156
119
|
|
|
157
120
|
def merge_configs(base: PermissionConfig, overlay: PermissionConfig) -> PermissionConfig:
|
|
@@ -175,22 +138,22 @@ def permission_scope(overlay_raw: dict[str, Any] | None) -> Generator[None, None
|
|
|
175
138
|
Each scope gets its own fresh "always" session memory, so agent approvals
|
|
176
139
|
don't leak to the global scope or other agents.
|
|
177
140
|
"""
|
|
178
|
-
global _config, _session_allowed
|
|
179
141
|
if not overlay_raw:
|
|
180
142
|
yield
|
|
181
143
|
return
|
|
182
144
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
145
|
+
ctx = get_ctx()
|
|
146
|
+
ctx.config_stack.append(ctx.perm_config)
|
|
147
|
+
ctx.session_stack.append(ctx.session_allowed)
|
|
148
|
+
ctx.session_allowed = set()
|
|
186
149
|
|
|
187
150
|
overlay = parse_permission_config(overlay_raw)
|
|
188
|
-
|
|
151
|
+
ctx.perm_config = merge_configs(ctx.perm_config, overlay)
|
|
189
152
|
try:
|
|
190
153
|
yield
|
|
191
154
|
finally:
|
|
192
|
-
|
|
193
|
-
|
|
155
|
+
ctx.perm_config = ctx.config_stack.pop()
|
|
156
|
+
ctx.session_allowed = ctx.session_stack.pop()
|
|
194
157
|
|
|
195
158
|
|
|
196
159
|
# ---------------------------------------------------------------------------
|
|
@@ -304,8 +267,9 @@ def _build_rules(category: str) -> list[PermissionRule]:
|
|
|
304
267
|
rules.extend(_SENSITIVE_FILE_RULES)
|
|
305
268
|
|
|
306
269
|
# Add user-configured rules
|
|
307
|
-
|
|
308
|
-
|
|
270
|
+
ctx = get_ctx()
|
|
271
|
+
if category in ctx.perm_config.categories:
|
|
272
|
+
rules.extend(ctx.perm_config.categories[category])
|
|
309
273
|
|
|
310
274
|
return rules
|
|
311
275
|
|
|
@@ -382,7 +346,7 @@ def _resolve_bash_compound(command: str) -> tuple[PermissionAction, str]:
|
|
|
382
346
|
def _resolve_bash_single(command: str) -> tuple[PermissionAction, str]:
|
|
383
347
|
"""Resolve permission for a single (non-compound) bash command."""
|
|
384
348
|
rules = _build_rules("bash")
|
|
385
|
-
result: PermissionAction = CATEGORY_DEFAULTS.get("bash",
|
|
349
|
+
result: PermissionAction = CATEGORY_DEFAULTS.get("bash", get_ctx().perm_config.default)
|
|
386
350
|
matched_pattern = "*"
|
|
387
351
|
|
|
388
352
|
for rule in rules:
|
|
@@ -419,11 +383,12 @@ def resolve_permission(
|
|
|
419
383
|
4. For others: walk rules (defaults + user config), last-match-wins
|
|
420
384
|
5. Fallback: category default, then global default
|
|
421
385
|
"""
|
|
422
|
-
|
|
386
|
+
ctx = get_ctx()
|
|
387
|
+
if ctx.skip_permissions:
|
|
423
388
|
return ("allow", "*")
|
|
424
389
|
|
|
425
390
|
# Check session memory
|
|
426
|
-
for cat, pattern in
|
|
391
|
+
for cat, pattern in ctx.session_allowed:
|
|
427
392
|
if cat == category and _match_rule(pattern, subject):
|
|
428
393
|
return ("allow", pattern)
|
|
429
394
|
|
|
@@ -433,7 +398,7 @@ def resolve_permission(
|
|
|
433
398
|
|
|
434
399
|
# All other categories
|
|
435
400
|
rules = _build_rules(category)
|
|
436
|
-
result: PermissionAction = CATEGORY_DEFAULTS.get(category,
|
|
401
|
+
result: PermissionAction = CATEGORY_DEFAULTS.get(category, ctx.perm_config.default)
|
|
437
402
|
matched_pattern = "*"
|
|
438
403
|
|
|
439
404
|
for rule in rules:
|
|
@@ -465,7 +430,8 @@ def check_permission(
|
|
|
465
430
|
return False
|
|
466
431
|
|
|
467
432
|
# action == "ask" -> prompt user
|
|
468
|
-
|
|
433
|
+
ctx = get_ctx()
|
|
434
|
+
with ctx.permission_lock:
|
|
469
435
|
# Re-check after acquiring lock (another thread may have resolved it)
|
|
470
436
|
action2, pattern2 = resolve_permission(category, subject)
|
|
471
437
|
if action2 == "allow":
|
|
@@ -474,25 +440,25 @@ def check_permission(
|
|
|
474
440
|
return False
|
|
475
441
|
|
|
476
442
|
# Pause Live and flush already-streamed content
|
|
477
|
-
if
|
|
478
|
-
|
|
479
|
-
if
|
|
480
|
-
|
|
443
|
+
if ctx.live:
|
|
444
|
+
ctx.live.stop()
|
|
445
|
+
if ctx.display:
|
|
446
|
+
ctx.display.flush()
|
|
481
447
|
|
|
482
448
|
title = f"{category}: {subject}" if subject else category
|
|
483
|
-
|
|
484
|
-
|
|
449
|
+
ctx.console.print()
|
|
450
|
+
ctx.console.print(Panel(
|
|
485
451
|
display_details,
|
|
486
452
|
title=f"[bold yellow]{title}[/bold yellow]",
|
|
487
453
|
border_style="yellow",
|
|
488
454
|
expand=False,
|
|
489
455
|
))
|
|
490
456
|
try:
|
|
491
|
-
answer =
|
|
457
|
+
answer = ctx.console.input(
|
|
492
458
|
"[bold yellow]Allow? (y)es once / (a)lways / (n)o:[/bold yellow] "
|
|
493
459
|
).strip().lower()
|
|
494
460
|
if answer in ("a", "always", "all"):
|
|
495
|
-
|
|
461
|
+
ctx.session_allowed.add((category, matched_pattern))
|
|
496
462
|
allowed = True
|
|
497
463
|
else:
|
|
498
464
|
allowed = answer in ("y", "yes", "s", "sim")
|
|
@@ -500,8 +466,8 @@ def check_permission(
|
|
|
500
466
|
allowed = False
|
|
501
467
|
|
|
502
468
|
# Resume Live display
|
|
503
|
-
if
|
|
504
|
-
|
|
505
|
-
|
|
469
|
+
if ctx.live:
|
|
470
|
+
ctx.live.start()
|
|
471
|
+
ctx.live._live_render._shape = None
|
|
506
472
|
|
|
507
473
|
return allowed
|
|
@@ -69,9 +69,7 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
|
|
|
69
69
|
_stalled = False
|
|
70
70
|
|
|
71
71
|
try:
|
|
72
|
-
from aru.
|
|
73
|
-
from aru.permissions import set_live as perm_set_live, set_display as perm_set_display
|
|
74
|
-
from aru.tools.tasklist import set_live as tasklist_set_live, set_display as tasklist_set_display
|
|
72
|
+
from aru.runtime import get_ctx
|
|
75
73
|
|
|
76
74
|
status = StatusBar(interval=3.0)
|
|
77
75
|
display = StreamingDisplay(status)
|
|
@@ -125,12 +123,9 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
|
|
|
125
123
|
|
|
126
124
|
run_output = None
|
|
127
125
|
with Live(display, console=console, refresh_per_second=10) as live:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
perm_set_display(display)
|
|
132
|
-
tasklist_set_live(live)
|
|
133
|
-
tasklist_set_display(display)
|
|
126
|
+
ctx = get_ctx()
|
|
127
|
+
ctx.live = live
|
|
128
|
+
ctx.display = display
|
|
134
129
|
accumulated = ""
|
|
135
130
|
_stall_counter = 0
|
|
136
131
|
_stalled = False
|
|
@@ -215,10 +210,8 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
|
|
|
215
210
|
)
|
|
216
211
|
break
|
|
217
212
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
perm_set_live(None)
|
|
221
|
-
perm_set_display(None)
|
|
213
|
+
ctx.live = None
|
|
214
|
+
ctx.display = None
|
|
222
215
|
|
|
223
216
|
if run_output and session and hasattr(run_output, "metrics"):
|
|
224
217
|
session.track_tokens(run_output.metrics)
|
|
@@ -239,16 +232,14 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
|
|
|
239
232
|
console.print(Markdown(remaining))
|
|
240
233
|
|
|
241
234
|
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
perm_set_display(None)
|
|
235
|
+
ctx = get_ctx()
|
|
236
|
+
ctx.live = None
|
|
237
|
+
ctx.display = None
|
|
246
238
|
console.print("\n[yellow]Interrupted.[/yellow]")
|
|
247
239
|
except Exception as e:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
perm_set_display(None)
|
|
240
|
+
ctx = get_ctx()
|
|
241
|
+
ctx.live = None
|
|
242
|
+
ctx.display = None
|
|
252
243
|
from rich.markup import escape
|
|
253
244
|
console.print(f"[red]Error: {escape(str(e))}[/red]")
|
|
254
245
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Centralised runtime context for Aru.
|
|
2
|
+
|
|
3
|
+
Replaces scattered module-level globals with a single RuntimeContext
|
|
4
|
+
accessible via ``contextvars.ContextVar``. This gives each asyncio task
|
|
5
|
+
(and each ``asyncio.to_thread`` call) its own isolated snapshot, which
|
|
6
|
+
means parallel agent runs and tests never share mutable state.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from aru.runtime import get_ctx, init_ctx
|
|
11
|
+
|
|
12
|
+
# At startup (cli.py):
|
|
13
|
+
ctx = init_ctx(console=console)
|
|
14
|
+
|
|
15
|
+
# In any tool / helper:
|
|
16
|
+
ctx = get_ctx()
|
|
17
|
+
ctx.live # Rich Live instance (or None)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import contextvars
|
|
23
|
+
import copy
|
|
24
|
+
import threading
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any, Callable
|
|
27
|
+
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── TaskStore (moved from tools/tasklist.py) ─────────────────────────
|
|
32
|
+
|
|
33
|
+
class TaskStore:
|
|
34
|
+
"""Thread-safe store for the current step's subtask list."""
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self._lock = threading.Lock()
|
|
38
|
+
self._tasks: list[dict] = []
|
|
39
|
+
self._created = False
|
|
40
|
+
|
|
41
|
+
def create(self, tasks: list[str]) -> list[dict]:
|
|
42
|
+
with self._lock:
|
|
43
|
+
self._tasks = [
|
|
44
|
+
{"index": i + 1, "description": desc, "status": "pending"}
|
|
45
|
+
for i, desc in enumerate(tasks)
|
|
46
|
+
]
|
|
47
|
+
self._created = True
|
|
48
|
+
return list(self._tasks)
|
|
49
|
+
|
|
50
|
+
def update(self, index: int, status: str) -> dict | None:
|
|
51
|
+
with self._lock:
|
|
52
|
+
for task in self._tasks:
|
|
53
|
+
if task["index"] == index:
|
|
54
|
+
task["status"] = status
|
|
55
|
+
return dict(task)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def get_all(self) -> list[dict]:
|
|
59
|
+
with self._lock:
|
|
60
|
+
return list(self._tasks)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_created(self) -> bool:
|
|
64
|
+
with self._lock:
|
|
65
|
+
return self._created
|
|
66
|
+
|
|
67
|
+
def reset(self) -> None:
|
|
68
|
+
with self._lock:
|
|
69
|
+
self._tasks = []
|
|
70
|
+
self._created = False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── PermissionConfig (imported lazily to avoid circular deps) ────────
|
|
74
|
+
|
|
75
|
+
def _default_perm_config():
|
|
76
|
+
from aru.permissions import PermissionConfig
|
|
77
|
+
return PermissionConfig()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ── RuntimeContext ───────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class RuntimeContext:
|
|
84
|
+
"""All mutable runtime state, grouped by domain."""
|
|
85
|
+
|
|
86
|
+
# -- Display --
|
|
87
|
+
console: Console = field(default_factory=Console)
|
|
88
|
+
live: Any = None
|
|
89
|
+
display: Any = None
|
|
90
|
+
|
|
91
|
+
# -- Model --
|
|
92
|
+
model_id: str = "claude-sonnet-4-5-20250929"
|
|
93
|
+
small_model_ref: str = "anthropic/claude-haiku-4-5"
|
|
94
|
+
|
|
95
|
+
# -- File operations --
|
|
96
|
+
on_file_mutation: Callable[[], None] | None = None
|
|
97
|
+
read_cache: dict[tuple, str] = field(default_factory=dict)
|
|
98
|
+
|
|
99
|
+
# -- Process tracking --
|
|
100
|
+
tracked_processes: list = field(default_factory=list)
|
|
101
|
+
subagent_counter: int = 0
|
|
102
|
+
subagent_counter_lock: threading.Lock = field(default_factory=threading.Lock)
|
|
103
|
+
|
|
104
|
+
# -- Custom agents --
|
|
105
|
+
custom_agent_defs: dict = field(default_factory=dict)
|
|
106
|
+
|
|
107
|
+
# -- Permissions --
|
|
108
|
+
perm_config: Any = field(default_factory=_default_perm_config)
|
|
109
|
+
session_allowed: set[tuple[str, str]] = field(default_factory=set)
|
|
110
|
+
skip_permissions: bool = False
|
|
111
|
+
permission_lock: threading.Lock = field(default_factory=threading.Lock)
|
|
112
|
+
config_stack: list = field(default_factory=list)
|
|
113
|
+
session_stack: list[set[tuple[str, str]]] = field(default_factory=list)
|
|
114
|
+
|
|
115
|
+
# -- Tasklist --
|
|
116
|
+
task_store: TaskStore = field(default_factory=TaskStore)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ── ContextVar plumbing ──────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
_runtime_ctx: contextvars.ContextVar[RuntimeContext] = contextvars.ContextVar("aru_runtime")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_ctx() -> RuntimeContext:
|
|
125
|
+
"""Return the current RuntimeContext. Raises LookupError if not initialised."""
|
|
126
|
+
return _runtime_ctx.get()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def set_ctx(ctx: RuntimeContext) -> contextvars.Token[RuntimeContext]:
|
|
130
|
+
"""Set *ctx* as the current RuntimeContext; return a reset token."""
|
|
131
|
+
return _runtime_ctx.set(ctx)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def init_ctx(console: Console | None = None, **kwargs: Any) -> RuntimeContext:
|
|
135
|
+
"""Create a new RuntimeContext, install it, and return it."""
|
|
136
|
+
ctx = RuntimeContext(console=console or Console(), **kwargs)
|
|
137
|
+
_runtime_ctx.set(ctx)
|
|
138
|
+
return ctx
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def fork_ctx() -> RuntimeContext:
|
|
142
|
+
"""Create an isolated copy of the current RuntimeContext for sub-agent use.
|
|
143
|
+
|
|
144
|
+
Permission state is deep-copied to prevent interleaving when multiple
|
|
145
|
+
sub-agents run concurrently via ``asyncio.gather``. Shared resources
|
|
146
|
+
(console, locks, tracked_processes) are kept by reference.
|
|
147
|
+
"""
|
|
148
|
+
original = get_ctx()
|
|
149
|
+
forked = copy.copy(original)
|
|
150
|
+
# Deep-copy mutable permission state for isolation
|
|
151
|
+
forked.config_stack = list(original.config_stack)
|
|
152
|
+
forked.session_stack = [s.copy() for s in original.session_stack]
|
|
153
|
+
forked.session_allowed = original.session_allowed.copy()
|
|
154
|
+
# Fresh read cache per sub-agent
|
|
155
|
+
forked.read_cache = {}
|
|
156
|
+
# Fresh task store per sub-agent
|
|
157
|
+
forked.task_store = TaskStore()
|
|
158
|
+
return forked
|