mycode-coding-agent 0.1.0__py3-none-any.whl
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.
- mycode/__init__.py +0 -0
- mycode/adapters/__init__.py +21 -0
- mycode/adapters/jsonl.py +692 -0
- mycode/agent/__init__.py +25 -0
- mycode/agent/events.py +111 -0
- mycode/agent/outcome.py +103 -0
- mycode/agent/progress.py +373 -0
- mycode/agent/runner.py +1481 -0
- mycode/application/__init__.py +38 -0
- mycode/application/agent_session.py +367 -0
- mycode/application/events.py +59 -0
- mycode/application/runtime.py +211 -0
- mycode/application/sessions.py +180 -0
- mycode/cli.py +840 -0
- mycode/config.py +355 -0
- mycode/context/__init__.py +1 -0
- mycode/context/artifacts.py +672 -0
- mycode/context/budget.py +752 -0
- mycode/context/builder.py +112 -0
- mycode/context/compact.py +795 -0
- mycode/context/tool_result_format.py +199 -0
- mycode/context/tool_result_retention.py +261 -0
- mycode/conversation.py +78 -0
- mycode/error_handling.py +481 -0
- mycode/event_format.py +147 -0
- mycode/instructions.py +285 -0
- mycode/llm.py +771 -0
- mycode/mcp/__init__.py +41 -0
- mycode/mcp/client.py +44 -0
- mycode/mcp/config.py +207 -0
- mycode/mcp/errors.py +302 -0
- mycode/mcp/manager.py +339 -0
- mycode/mcp/models.py +20 -0
- mycode/mcp/result_adapter.py +58 -0
- mycode/mcp/tool_adapter.py +145 -0
- mycode/mcp/trust.py +313 -0
- mycode/memory.py +570 -0
- mycode/memory_context.py +245 -0
- mycode/messages.py +63 -0
- mycode/observability.py +28 -0
- mycode/permissions.py +262 -0
- mycode/persistence/__init__.py +1 -0
- mycode/persistence/filesystem.py +291 -0
- mycode/persistence/project_storage.py +208 -0
- mycode/persistence/session_lock.py +138 -0
- mycode/persistence/session_store.py +503 -0
- mycode/presentation/__init__.py +1 -0
- mycode/presentation/cli/__init__.py +14 -0
- mycode/presentation/cli/confirmer.py +116 -0
- mycode/presentation/cli/mcp_trust.py +61 -0
- mycode/presentation/cli/presenter.py +320 -0
- mycode/presentation/cli/session_menu.py +146 -0
- mycode/presentation/cli/subagent_observer.py +124 -0
- mycode/presentation/command_format.py +90 -0
- mycode/presentation/commands.py +95 -0
- mycode/presentation/tui/__init__.py +6 -0
- mycode/presentation/tui/app.py +1351 -0
- mycode/presentation/tui/interactions.py +253 -0
- mycode/presentation/tui/presenter.py +266 -0
- mycode/presentation/tui/screens.py +305 -0
- mycode/presentation/tui/widgets.py +214 -0
- mycode/project.py +22 -0
- mycode/prompts.py +181 -0
- mycode/reasoning.py +40 -0
- mycode/session.py +86 -0
- mycode/skills/__init__.py +27 -0
- mycode/skills/builtin/database-recovery/SKILL.md +138 -0
- mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
- mycode/skills/registry.py +295 -0
- mycode/skills/state.py +68 -0
- mycode/subagents/__init__.py +1 -0
- mycode/subagents/audit.py +212 -0
- mycode/subagents/concurrency.py +124 -0
- mycode/subagents/contracts.py +421 -0
- mycode/subagents/delegate.py +80 -0
- mycode/subagents/delegation.py +128 -0
- mycode/subagents/lifecycle.py +86 -0
- mycode/subagents/limits.py +7 -0
- mycode/subagents/observability.py +150 -0
- mycode/subagents/persistence.py +152 -0
- mycode/subagents/profiles.py +184 -0
- mycode/subagents/prompts.py +67 -0
- mycode/subagents/results.py +178 -0
- mycode/subagents/runtime.py +528 -0
- mycode/subagents/snapshots.py +211 -0
- mycode/subagents/tool_batch.py +260 -0
- mycode/tools/__init__.py +81 -0
- mycode/tools/base.py +222 -0
- mycode/tools/bounds.py +14 -0
- mycode/tools/command_executor.py +167 -0
- mycode/tools/command_output.py +166 -0
- mycode/tools/command_risk.py +596 -0
- mycode/tools/defaults.py +59 -0
- mycode/tools/edit_file.py +524 -0
- mycode/tools/file_mutation.py +30 -0
- mycode/tools/glob.py +247 -0
- mycode/tools/grep.py +324 -0
- mycode/tools/ignore.py +122 -0
- mycode/tools/inspect_changes.py +269 -0
- mycode/tools/load_skill.py +92 -0
- mycode/tools/memory.py +264 -0
- mycode/tools/path_permissions.py +78 -0
- mycode/tools/patterns.py +48 -0
- mycode/tools/permission_metadata.py +27 -0
- mycode/tools/process_tree.py +166 -0
- mycode/tools/read_file.py +242 -0
- mycode/tools/read_skill_resource.py +93 -0
- mycode/tools/registry.py +279 -0
- mycode/tools/run_command.py +237 -0
- mycode/tools/run_skill_script.py +206 -0
- mycode/tools/run_validation.py +107 -0
- mycode/tools/submit_result.py +93 -0
- mycode/tools/text.py +15 -0
- mycode/tools/validation_command.py +377 -0
- mycode/tools/workspace.py +33 -0
- mycode/tools/write_file.py +169 -0
- mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
- mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
- mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
mycode/config.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Iterator, Mapping
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from dotenv import dotenv_values
|
|
8
|
+
|
|
9
|
+
MYCODE_CONFIG_DIR_NAME = ".mycode"
|
|
10
|
+
MYCODE_ENV_FILE_NAME = ".env"
|
|
11
|
+
DEFAULT_LLM_CONTEXT_WINDOW_TOKENS = 128000
|
|
12
|
+
DEFAULT_LLM_RESERVED_OUTPUT_TOKENS = 8192
|
|
13
|
+
DEFAULT_LLM_CONTEXT_SAFETY_MARGIN_TOKENS = 4096
|
|
14
|
+
DEFAULT_LLM_MEMORY_CONTEXT_TOKENS = 2048
|
|
15
|
+
DEFAULT_LLM_STREAM_INCLUDE_USAGE = True
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ReasoningEffort = Literal["high", "max"]
|
|
19
|
+
SUPPORTED_REASONING_EFFORTS: frozenset[str] = frozenset({"high", "max"})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MissingLLMConfigError(ValueError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _RedactedConfigValues(Mapping[str, str | None]):
|
|
27
|
+
def __init__(self, values: Mapping[str, str | None]) -> None:
|
|
28
|
+
self._values = values
|
|
29
|
+
|
|
30
|
+
def __getitem__(self, key: str) -> str | None:
|
|
31
|
+
return self._values[key]
|
|
32
|
+
|
|
33
|
+
def __iter__(self) -> Iterator[str]:
|
|
34
|
+
return iter(self._values)
|
|
35
|
+
|
|
36
|
+
def __len__(self) -> int:
|
|
37
|
+
return len(self._values)
|
|
38
|
+
|
|
39
|
+
def __repr__(self) -> str:
|
|
40
|
+
return "<redacted config values>"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class LLMConfig:
|
|
45
|
+
api_key: str = field(repr=False)
|
|
46
|
+
base_url: str
|
|
47
|
+
model: str
|
|
48
|
+
compact_model: str | None = None
|
|
49
|
+
subagent_model: str | None = None
|
|
50
|
+
context_window_tokens: int = DEFAULT_LLM_CONTEXT_WINDOW_TOKENS
|
|
51
|
+
reserved_output_tokens: int = DEFAULT_LLM_RESERVED_OUTPUT_TOKENS
|
|
52
|
+
context_safety_margin_tokens: int = DEFAULT_LLM_CONTEXT_SAFETY_MARGIN_TOKENS
|
|
53
|
+
memory_context_tokens: int = DEFAULT_LLM_MEMORY_CONTEXT_TOKENS
|
|
54
|
+
stream_include_usage: bool = DEFAULT_LLM_STREAM_INCLUDE_USAGE
|
|
55
|
+
thinking_enabled: bool | None = None
|
|
56
|
+
reasoning_effort: ReasoningEffort | None = None
|
|
57
|
+
max_output_tokens: int | None = None
|
|
58
|
+
|
|
59
|
+
def __post_init__(self) -> None:
|
|
60
|
+
main_model = self.model.strip()
|
|
61
|
+
if main_model == "":
|
|
62
|
+
raise ValueError("MYCODE_MODEL must not be empty.")
|
|
63
|
+
|
|
64
|
+
compact_model = self.compact_model
|
|
65
|
+
subagent_model = self.subagent_model
|
|
66
|
+
object.__setattr__(self, "model", main_model)
|
|
67
|
+
object.__setattr__(
|
|
68
|
+
self,
|
|
69
|
+
"compact_model",
|
|
70
|
+
(
|
|
71
|
+
main_model
|
|
72
|
+
if compact_model is None or compact_model.strip() == ""
|
|
73
|
+
else compact_model.strip()
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
object.__setattr__(
|
|
77
|
+
self,
|
|
78
|
+
"subagent_model",
|
|
79
|
+
main_model
|
|
80
|
+
if subagent_model is None or subagent_model.strip() == ""
|
|
81
|
+
else subagent_model.strip(),
|
|
82
|
+
)
|
|
83
|
+
if self.reasoning_effort is not None:
|
|
84
|
+
normalized_effort = self.reasoning_effort.strip().lower()
|
|
85
|
+
if normalized_effort not in SUPPORTED_REASONING_EFFORTS:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"LLM_REASONING_EFFORT must be one of: high, max."
|
|
88
|
+
)
|
|
89
|
+
object.__setattr__(self, "reasoning_effort", normalized_effort)
|
|
90
|
+
if self.thinking_enabled is True and self.reasoning_effort is None:
|
|
91
|
+
object.__setattr__(self, "reasoning_effort", "high")
|
|
92
|
+
if self.thinking_enabled is not True and self.reasoning_effort is not None:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
"LLM_REASONING_EFFORT requires LLM_THINKING_ENABLED=true."
|
|
95
|
+
)
|
|
96
|
+
if self.max_output_tokens is not None:
|
|
97
|
+
if self.max_output_tokens < 1:
|
|
98
|
+
raise ValueError("LLM_MAX_OUTPUT_TOKENS must be at least 1.")
|
|
99
|
+
if self.max_output_tokens > self.reserved_output_tokens:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
"LLM_MAX_OUTPUT_TOKENS must not exceed "
|
|
102
|
+
"LLM_RESERVED_OUTPUT_TOKENS."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def default_user_env_file() -> Path:
|
|
107
|
+
return Path.home() / MYCODE_CONFIG_DIR_NAME / MYCODE_ENV_FILE_NAME
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def project_env_file(workspace_root: str | Path) -> Path:
|
|
111
|
+
return Path(workspace_root) / MYCODE_CONFIG_DIR_NAME / MYCODE_ENV_FILE_NAME
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load_layered_environment(
|
|
115
|
+
env_file: str | Path | None = None,
|
|
116
|
+
*,
|
|
117
|
+
workspace_root: str | Path | None = None,
|
|
118
|
+
environ: Mapping[str, str] | None = None,
|
|
119
|
+
) -> dict[str, str]:
|
|
120
|
+
user_values = dotenv_values(
|
|
121
|
+
default_user_env_file() if env_file is None else env_file
|
|
122
|
+
)
|
|
123
|
+
project_values = (
|
|
124
|
+
{}
|
|
125
|
+
if workspace_root is None
|
|
126
|
+
else dotenv_values(project_env_file(workspace_root))
|
|
127
|
+
)
|
|
128
|
+
process_values = os.environ if environ is None else environ
|
|
129
|
+
merged: dict[str, str] = {}
|
|
130
|
+
for values in (user_values, project_values, process_values):
|
|
131
|
+
for name, value in values.items():
|
|
132
|
+
if value is not None and value.strip() != "":
|
|
133
|
+
merged[name] = value
|
|
134
|
+
return merged
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def load_llm_config(
|
|
138
|
+
env_file: str | Path | None = None,
|
|
139
|
+
*,
|
|
140
|
+
workspace_root: str | Path | None = None,
|
|
141
|
+
) -> LLMConfig:
|
|
142
|
+
values = _RedactedConfigValues(
|
|
143
|
+
load_layered_environment(
|
|
144
|
+
env_file,
|
|
145
|
+
workspace_root=workspace_root,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
api_key = _required_env_value(
|
|
150
|
+
"MYCODE_API_KEY",
|
|
151
|
+
values,
|
|
152
|
+
)
|
|
153
|
+
base_url = _required_env_value(
|
|
154
|
+
"MYCODE_BASE_URL",
|
|
155
|
+
values,
|
|
156
|
+
)
|
|
157
|
+
model = _required_env_value(
|
|
158
|
+
"MYCODE_MODEL",
|
|
159
|
+
values,
|
|
160
|
+
)
|
|
161
|
+
compact_model = _env_value(
|
|
162
|
+
"MYCODE_COMPACT_MODEL",
|
|
163
|
+
model,
|
|
164
|
+
values,
|
|
165
|
+
)
|
|
166
|
+
subagent_model = _env_value(
|
|
167
|
+
"MYCODE_SUBAGENT_MODEL",
|
|
168
|
+
model,
|
|
169
|
+
values,
|
|
170
|
+
)
|
|
171
|
+
context_window_tokens = _int_env_value(
|
|
172
|
+
"LLM_CONTEXT_WINDOW_TOKENS",
|
|
173
|
+
DEFAULT_LLM_CONTEXT_WINDOW_TOKENS,
|
|
174
|
+
values,
|
|
175
|
+
minimum=1,
|
|
176
|
+
)
|
|
177
|
+
reserved_output_tokens = _int_env_value(
|
|
178
|
+
"LLM_RESERVED_OUTPUT_TOKENS",
|
|
179
|
+
DEFAULT_LLM_RESERVED_OUTPUT_TOKENS,
|
|
180
|
+
values,
|
|
181
|
+
minimum=0,
|
|
182
|
+
)
|
|
183
|
+
context_safety_margin_tokens = _int_env_value(
|
|
184
|
+
"LLM_CONTEXT_SAFETY_MARGIN_TOKENS",
|
|
185
|
+
DEFAULT_LLM_CONTEXT_SAFETY_MARGIN_TOKENS,
|
|
186
|
+
values,
|
|
187
|
+
minimum=0,
|
|
188
|
+
)
|
|
189
|
+
memory_context_tokens = _int_env_value(
|
|
190
|
+
"LLM_MEMORY_CONTEXT_TOKENS",
|
|
191
|
+
DEFAULT_LLM_MEMORY_CONTEXT_TOKENS,
|
|
192
|
+
values,
|
|
193
|
+
minimum=0,
|
|
194
|
+
)
|
|
195
|
+
stream_include_usage = _bool_env_value(
|
|
196
|
+
"LLM_STREAM_INCLUDE_USAGE",
|
|
197
|
+
DEFAULT_LLM_STREAM_INCLUDE_USAGE,
|
|
198
|
+
values,
|
|
199
|
+
)
|
|
200
|
+
thinking_enabled = _optional_bool_env_value(
|
|
201
|
+
"LLM_THINKING_ENABLED",
|
|
202
|
+
values,
|
|
203
|
+
)
|
|
204
|
+
reasoning_effort = _optional_reasoning_effort_env_value(
|
|
205
|
+
"LLM_REASONING_EFFORT",
|
|
206
|
+
values,
|
|
207
|
+
)
|
|
208
|
+
max_output_tokens = _optional_int_env_value(
|
|
209
|
+
"LLM_MAX_OUTPUT_TOKENS",
|
|
210
|
+
values,
|
|
211
|
+
minimum=1,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if reserved_output_tokens + context_safety_margin_tokens >= context_window_tokens:
|
|
215
|
+
raise ValueError(
|
|
216
|
+
"LLM_RESERVED_OUTPUT_TOKENS and LLM_CONTEXT_SAFETY_MARGIN_TOKENS "
|
|
217
|
+
"must leave at least 1 input token."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return LLMConfig(
|
|
221
|
+
api_key=api_key,
|
|
222
|
+
base_url=base_url,
|
|
223
|
+
model=model,
|
|
224
|
+
compact_model=compact_model,
|
|
225
|
+
subagent_model=subagent_model,
|
|
226
|
+
context_window_tokens=context_window_tokens,
|
|
227
|
+
reserved_output_tokens=reserved_output_tokens,
|
|
228
|
+
context_safety_margin_tokens=context_safety_margin_tokens,
|
|
229
|
+
memory_context_tokens=memory_context_tokens,
|
|
230
|
+
stream_include_usage=stream_include_usage,
|
|
231
|
+
thinking_enabled=thinking_enabled,
|
|
232
|
+
reasoning_effort=reasoning_effort,
|
|
233
|
+
max_output_tokens=max_output_tokens,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _required_env_value(
|
|
238
|
+
name: str,
|
|
239
|
+
*value_maps: Mapping[str, str | None],
|
|
240
|
+
) -> str:
|
|
241
|
+
value = _env_value(name, "", *value_maps)
|
|
242
|
+
|
|
243
|
+
if value == "":
|
|
244
|
+
raise MissingLLMConfigError(f"Missing required environment variable: {name}")
|
|
245
|
+
|
|
246
|
+
return value
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _env_value(
|
|
250
|
+
name: str,
|
|
251
|
+
default: str,
|
|
252
|
+
*value_maps: Mapping[str, str | None],
|
|
253
|
+
) -> str:
|
|
254
|
+
values = [value_map.get(name) for value_map in value_maps]
|
|
255
|
+
|
|
256
|
+
for value in values:
|
|
257
|
+
if value is None:
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
value = value.strip()
|
|
261
|
+
if value != "":
|
|
262
|
+
return value
|
|
263
|
+
|
|
264
|
+
return default
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _int_env_value(
|
|
268
|
+
name: str,
|
|
269
|
+
default: int,
|
|
270
|
+
*value_maps: Mapping[str, str | None],
|
|
271
|
+
minimum: int,
|
|
272
|
+
) -> int:
|
|
273
|
+
raw_value = _env_value(name, str(default), *value_maps)
|
|
274
|
+
try:
|
|
275
|
+
value = int(raw_value)
|
|
276
|
+
except ValueError as error:
|
|
277
|
+
raise ValueError(f"{name} must be an integer.") from error
|
|
278
|
+
|
|
279
|
+
if value < minimum:
|
|
280
|
+
raise ValueError(f"{name} must be at least {minimum}.")
|
|
281
|
+
|
|
282
|
+
return value
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _bool_env_value(
|
|
286
|
+
name: str,
|
|
287
|
+
default: bool,
|
|
288
|
+
*value_maps: Mapping[str, str | None],
|
|
289
|
+
) -> bool:
|
|
290
|
+
raw_value = _env_value(name, str(default).lower(), *value_maps).lower()
|
|
291
|
+
if raw_value in {"1", "true", "yes", "on"}:
|
|
292
|
+
return True
|
|
293
|
+
if raw_value in {"0", "false", "no", "off"}:
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
raise ValueError(f"{name} must be true or false.")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _optional_bool_env_value(
|
|
300
|
+
name: str,
|
|
301
|
+
*value_maps: Mapping[str, str | None],
|
|
302
|
+
) -> bool | None:
|
|
303
|
+
raw_value = _optional_env_value(name, *value_maps)
|
|
304
|
+
if raw_value is None:
|
|
305
|
+
return None
|
|
306
|
+
normalized = raw_value.lower()
|
|
307
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
308
|
+
return True
|
|
309
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
310
|
+
return False
|
|
311
|
+
raise ValueError(f"{name} must be true or false.")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _optional_int_env_value(
|
|
315
|
+
name: str,
|
|
316
|
+
*value_maps: Mapping[str, str | None],
|
|
317
|
+
minimum: int,
|
|
318
|
+
) -> int | None:
|
|
319
|
+
raw_value = _optional_env_value(name, *value_maps)
|
|
320
|
+
if raw_value is None:
|
|
321
|
+
return None
|
|
322
|
+
try:
|
|
323
|
+
value = int(raw_value)
|
|
324
|
+
except ValueError as error:
|
|
325
|
+
raise ValueError(f"{name} must be an integer.") from error
|
|
326
|
+
if value < minimum:
|
|
327
|
+
raise ValueError(f"{name} must be at least {minimum}.")
|
|
328
|
+
return value
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _optional_reasoning_effort_env_value(
|
|
332
|
+
name: str,
|
|
333
|
+
*value_maps: Mapping[str, str | None],
|
|
334
|
+
) -> ReasoningEffort | None:
|
|
335
|
+
raw_value = _optional_env_value(name, *value_maps)
|
|
336
|
+
if raw_value is None:
|
|
337
|
+
return None
|
|
338
|
+
normalized = raw_value.lower()
|
|
339
|
+
if normalized not in SUPPORTED_REASONING_EFFORTS:
|
|
340
|
+
raise ValueError(f"{name} must be one of: high, max.")
|
|
341
|
+
return normalized # type: ignore[return-value]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _optional_env_value(
|
|
345
|
+
name: str,
|
|
346
|
+
*value_maps: Mapping[str, str | None],
|
|
347
|
+
) -> str | None:
|
|
348
|
+
values = [value_map.get(name) for value_map in value_maps]
|
|
349
|
+
for value in values:
|
|
350
|
+
if value is None:
|
|
351
|
+
continue
|
|
352
|
+
normalized = value.strip()
|
|
353
|
+
if normalized != "":
|
|
354
|
+
return normalized
|
|
355
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Context construction, compaction, artifacts, and tool-result handling."""
|