agent-dispatch 0.2.0__tar.gz → 0.2.2__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.
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/PKG-INFO +1 -1
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/pyproject.toml +1 -1
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/src/agent_dispatch/__init__.py +1 -1
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/src/agent_dispatch/cli.py +48 -19
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/src/agent_dispatch/models.py +15 -5
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/src/agent_dispatch/runner.py +51 -12
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/src/agent_dispatch/server.py +30 -15
- agent_dispatch-0.2.2/tests/test_cli.py +415 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/tests/test_models.py +16 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/tests/test_runner.py +93 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/tests/test_server.py +49 -1
- agent_dispatch-0.2.0/tests/test_cli.py +0 -203
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/.github/dependabot.yml +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/.github/workflows/ci.yml +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/.github/workflows/publish.yml +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/.gitignore +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/LICENSE +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/README.md +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/SECURITY.md +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/agents.example.yaml +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/assets/mascot.png +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/src/agent_dispatch/cache.py +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/src/agent_dispatch/config.py +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/tests/__init__.py +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/tests/conftest.py +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/tests/test_cache.py +0 -0
- {agent_dispatch-0.2.0 → agent_dispatch-0.2.2}/tests/test_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-dispatch
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: MCP server that lets Claude Code agents delegate tasks to agents in other project directories
|
|
5
5
|
Project-URL: Homepage, https://github.com/ginkida/agent-dispatch
|
|
6
6
|
Project-URL: Repository, https://github.com/ginkida/agent-dispatch
|
|
@@ -8,11 +8,31 @@ import subprocess
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
|
+
import yaml
|
|
12
|
+
from pydantic import ValidationError
|
|
11
13
|
|
|
12
14
|
from .config import auto_describe, config_path, load_config, save_config
|
|
13
15
|
from .models import AgentConfig, DispatchConfig, check_permission_mode, validate_agent_name
|
|
14
16
|
|
|
15
17
|
|
|
18
|
+
def _load_or_exit() -> DispatchConfig:
|
|
19
|
+
"""Load config, exiting with a friendly error on malformed YAML or schema."""
|
|
20
|
+
try:
|
|
21
|
+
return load_config()
|
|
22
|
+
except ValidationError as e:
|
|
23
|
+
click.echo(click.style(
|
|
24
|
+
f"Error: config at {config_path()} has an invalid schema:", fg="red"
|
|
25
|
+
))
|
|
26
|
+
click.echo(str(e))
|
|
27
|
+
raise SystemExit(1)
|
|
28
|
+
except yaml.YAMLError as e:
|
|
29
|
+
click.echo(click.style(
|
|
30
|
+
f"Error: config at {config_path()} is not valid YAML:", fg="red"
|
|
31
|
+
))
|
|
32
|
+
click.echo(str(e))
|
|
33
|
+
raise SystemExit(1)
|
|
34
|
+
|
|
35
|
+
|
|
16
36
|
@click.group()
|
|
17
37
|
@click.version_option(package_name="agent-dispatch")
|
|
18
38
|
def cli() -> None:
|
|
@@ -107,7 +127,7 @@ def add(
|
|
|
107
127
|
click.echo(f"Error: {e}")
|
|
108
128
|
raise SystemExit(1)
|
|
109
129
|
|
|
110
|
-
config =
|
|
130
|
+
config = _load_or_exit()
|
|
111
131
|
dir_path = Path(directory).resolve()
|
|
112
132
|
|
|
113
133
|
if name in config.agents:
|
|
@@ -126,9 +146,9 @@ def add(
|
|
|
126
146
|
max_budget_usd=max_budget,
|
|
127
147
|
permission_mode=permission_mode,
|
|
128
148
|
allowed_tools=[t.strip() for t in allowed_tools.split(",") if t.strip()]
|
|
129
|
-
if allowed_tools else
|
|
149
|
+
if allowed_tools else None,
|
|
130
150
|
disallowed_tools=[t.strip() for t in disallowed_tools.split(",") if t.strip()]
|
|
131
|
-
if disallowed_tools else
|
|
151
|
+
if disallowed_tools else None,
|
|
132
152
|
)
|
|
133
153
|
if warning := check_permission_mode(permission_mode):
|
|
134
154
|
click.echo(click.style(f"Warning: {warning}", fg="yellow"))
|
|
@@ -141,7 +161,7 @@ def add(
|
|
|
141
161
|
@click.argument("name")
|
|
142
162
|
def remove(name: str) -> None:
|
|
143
163
|
"""Remove an agent."""
|
|
144
|
-
config =
|
|
164
|
+
config = _load_or_exit()
|
|
145
165
|
if name not in config.agents:
|
|
146
166
|
click.echo(f"Agent '{name}' not found.")
|
|
147
167
|
raise SystemExit(1)
|
|
@@ -154,14 +174,20 @@ def remove(name: str) -> None:
|
|
|
154
174
|
@cli.command("list")
|
|
155
175
|
def list_agents() -> None:
|
|
156
176
|
"""List configured agents with health status."""
|
|
157
|
-
config =
|
|
177
|
+
config = _load_or_exit()
|
|
158
178
|
if not config.agents:
|
|
159
179
|
click.echo("No agents configured. Run: agent-dispatch add <name> <directory>")
|
|
160
180
|
return
|
|
161
181
|
|
|
162
182
|
for name, agent in config.agents.items():
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
try:
|
|
184
|
+
healthy = agent.directory.is_dir()
|
|
185
|
+
status_label = "OK" if healthy else "NOT FOUND"
|
|
186
|
+
status_color = "green" if healthy else "red"
|
|
187
|
+
except OSError:
|
|
188
|
+
status_label = "UNREADABLE"
|
|
189
|
+
status_color = "red"
|
|
190
|
+
status = click.style(status_label, fg=status_color)
|
|
165
191
|
click.echo(f" {name} [{status}]")
|
|
166
192
|
click.echo(f" dir: {agent.directory}")
|
|
167
193
|
click.echo(f" desc: {agent.description}")
|
|
@@ -176,10 +202,12 @@ def list_agents() -> None:
|
|
|
176
202
|
click.echo(f" config: {', '.join(extras)}")
|
|
177
203
|
if agent.permission_mode:
|
|
178
204
|
click.echo(f" permission_mode: {agent.permission_mode}")
|
|
179
|
-
if agent.allowed_tools:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
205
|
+
if agent.allowed_tools is not None:
|
|
206
|
+
rendered = ", ".join(agent.allowed_tools) if agent.allowed_tools else "(none)"
|
|
207
|
+
click.echo(f" allowed_tools: {rendered}")
|
|
208
|
+
if agent.disallowed_tools is not None:
|
|
209
|
+
rendered = ", ".join(agent.disallowed_tools) if agent.disallowed_tools else "(none)"
|
|
210
|
+
click.echo(f" disallowed_tools: {rendered}")
|
|
183
211
|
click.echo()
|
|
184
212
|
|
|
185
213
|
|
|
@@ -214,7 +242,7 @@ def update(
|
|
|
214
242
|
disallowed_tools: str | None,
|
|
215
243
|
) -> None:
|
|
216
244
|
"""Update an existing agent's configuration."""
|
|
217
|
-
config =
|
|
245
|
+
config = _load_or_exit()
|
|
218
246
|
if name not in config.agents:
|
|
219
247
|
click.echo(f"Agent '{name}' not found. Run 'agent-dispatch list' to see agents.")
|
|
220
248
|
raise SystemExit(1)
|
|
@@ -229,26 +257,27 @@ def update(
|
|
|
229
257
|
agent.timeout = timeout
|
|
230
258
|
updated.append("timeout")
|
|
231
259
|
if model is not None:
|
|
232
|
-
agent.model = None if model.lower()
|
|
260
|
+
agent.model = None if model.strip().lower() in ("none", "") else model
|
|
233
261
|
updated.append("model")
|
|
234
262
|
if max_budget is not None:
|
|
235
263
|
agent.max_budget_usd = None if max_budget == 0 else max_budget
|
|
236
264
|
updated.append("max_budget_usd")
|
|
237
265
|
if permission_mode is not None:
|
|
238
|
-
|
|
266
|
+
stripped = permission_mode.strip()
|
|
267
|
+
effective = None if stripped.lower() in ("none", "") else stripped
|
|
239
268
|
agent.permission_mode = effective
|
|
240
269
|
if warning := check_permission_mode(effective):
|
|
241
270
|
click.echo(click.style(f"Warning: {warning}", fg="yellow"))
|
|
242
271
|
updated.append("permission_mode")
|
|
243
272
|
if allowed_tools is not None:
|
|
244
|
-
if allowed_tools.lower()
|
|
245
|
-
agent.allowed_tools =
|
|
273
|
+
if allowed_tools.strip().lower() in ("none", ""):
|
|
274
|
+
agent.allowed_tools = None
|
|
246
275
|
else:
|
|
247
276
|
agent.allowed_tools = [t.strip() for t in allowed_tools.split(",") if t.strip()]
|
|
248
277
|
updated.append("allowed_tools")
|
|
249
278
|
if disallowed_tools is not None:
|
|
250
|
-
if disallowed_tools.lower()
|
|
251
|
-
agent.disallowed_tools =
|
|
279
|
+
if disallowed_tools.strip().lower() in ("none", ""):
|
|
280
|
+
agent.disallowed_tools = None
|
|
252
281
|
else:
|
|
253
282
|
agent.disallowed_tools = [
|
|
254
283
|
t.strip() for t in disallowed_tools.split(",") if t.strip()
|
|
@@ -268,7 +297,7 @@ def update(
|
|
|
268
297
|
@click.argument("task", default="What project is this? Describe in one sentence.")
|
|
269
298
|
def test(name: str, task: str) -> None:
|
|
270
299
|
"""Test an agent by dispatching a task."""
|
|
271
|
-
config =
|
|
300
|
+
config = _load_or_exit()
|
|
272
301
|
if name not in config.agents:
|
|
273
302
|
click.echo(f"Agent '{name}' not found. Run 'agent-dispatch list' to see agents.")
|
|
274
303
|
raise SystemExit(1)
|
|
@@ -15,14 +15,24 @@ KNOWN_PERMISSION_MODES = frozenset({
|
|
|
15
15
|
|
|
16
16
|
def check_permission_mode(mode: str | None) -> str | None:
|
|
17
17
|
"""Return a warning message if mode is unknown, else None."""
|
|
18
|
-
if
|
|
18
|
+
if not mode:
|
|
19
|
+
return None
|
|
20
|
+
trimmed = mode.strip()
|
|
21
|
+
if not trimmed:
|
|
22
|
+
return None
|
|
23
|
+
if trimmed not in KNOWN_PERMISSION_MODES:
|
|
19
24
|
known = ", ".join(sorted(KNOWN_PERMISSION_MODES))
|
|
20
|
-
return f"Unknown permission_mode: {
|
|
25
|
+
return f"Unknown permission_mode: {trimmed!r}. Known values: {known}"
|
|
21
26
|
return None
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
class AgentConfig(BaseModel):
|
|
25
|
-
"""Configuration for a single agent.
|
|
30
|
+
"""Configuration for a single agent.
|
|
31
|
+
|
|
32
|
+
`allowed_tools` / `disallowed_tools` use `None` to mean
|
|
33
|
+
"inherit from settings.default_*" and `[]` to mean "explicitly empty
|
|
34
|
+
(override defaults to no tools)".
|
|
35
|
+
"""
|
|
26
36
|
|
|
27
37
|
directory: Path
|
|
28
38
|
description: str = ""
|
|
@@ -30,8 +40,8 @@ class AgentConfig(BaseModel):
|
|
|
30
40
|
max_budget_usd: float | None = None
|
|
31
41
|
model: str | None = None
|
|
32
42
|
permission_mode: str | None = None
|
|
33
|
-
allowed_tools: list[str] =
|
|
34
|
-
disallowed_tools: list[str] =
|
|
43
|
+
allowed_tools: list[str] | None = None
|
|
44
|
+
disallowed_tools: list[str] | None = None
|
|
35
45
|
|
|
36
46
|
@field_validator("directory", mode="before")
|
|
37
47
|
@classmethod
|
|
@@ -29,9 +29,14 @@ _PERMISSION_PATTERNS = [
|
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def _classify_error(error_text:
|
|
33
|
-
"""Classify an error message into a category.
|
|
34
|
-
|
|
32
|
+
def _classify_error(error_text: object) -> str:
|
|
33
|
+
"""Classify an error message into a category.
|
|
34
|
+
|
|
35
|
+
Accepts any type and coerces to string — some claude CLI error paths
|
|
36
|
+
produce non-string values (None, dict) that would crash `.lower()`.
|
|
37
|
+
"""
|
|
38
|
+
text = str(error_text) if error_text else ""
|
|
39
|
+
lower = text.lower()
|
|
35
40
|
for pattern in _PERMISSION_PATTERNS:
|
|
36
41
|
if pattern in lower:
|
|
37
42
|
return "permission"
|
|
@@ -101,11 +106,18 @@ def _build_command(
|
|
|
101
106
|
if permission_mode:
|
|
102
107
|
cmd.extend(["--permission-mode", permission_mode])
|
|
103
108
|
|
|
104
|
-
|
|
109
|
+
# None = inherit from settings; [] = explicitly empty (no inheritance)
|
|
110
|
+
allowed = (
|
|
111
|
+
agent.allowed_tools if agent.allowed_tools is not None
|
|
112
|
+
else settings.default_allowed_tools
|
|
113
|
+
)
|
|
105
114
|
for tool in allowed:
|
|
106
115
|
cmd.extend(["--allowedTools", tool])
|
|
107
116
|
|
|
108
|
-
disallowed =
|
|
117
|
+
disallowed = (
|
|
118
|
+
agent.disallowed_tools if agent.disallowed_tools is not None
|
|
119
|
+
else settings.default_disallowed_tools
|
|
120
|
+
)
|
|
109
121
|
for tool in disallowed:
|
|
110
122
|
cmd.extend(["--disallowedTools", tool])
|
|
111
123
|
|
|
@@ -227,22 +239,36 @@ def dispatch(
|
|
|
227
239
|
data = json.loads(proc.stdout)
|
|
228
240
|
except json.JSONDecodeError:
|
|
229
241
|
# Fallback: treat stdout as plain text
|
|
242
|
+
success = proc.returncode == 0
|
|
243
|
+
text = proc.stdout.strip()
|
|
244
|
+
if success:
|
|
245
|
+
return DispatchResult(agent=agent_name, success=True, result=text)
|
|
246
|
+
error_text = text or f"claude exited with code {proc.returncode} (non-JSON output)"
|
|
247
|
+
error_type = _classify_error(error_text)
|
|
248
|
+
if error_type == "permission":
|
|
249
|
+
error_text += _permission_hint(agent_name)
|
|
230
250
|
return DispatchResult(
|
|
231
251
|
agent=agent_name,
|
|
232
|
-
success=
|
|
233
|
-
result=
|
|
252
|
+
success=False,
|
|
253
|
+
result=text,
|
|
254
|
+
error=error_text,
|
|
255
|
+
error_type=error_type,
|
|
234
256
|
)
|
|
235
257
|
|
|
236
258
|
is_error = data.get("is_error", False)
|
|
237
259
|
if is_error:
|
|
238
|
-
|
|
260
|
+
raw_result = data.get("result", "")
|
|
261
|
+
error_text = str(raw_result) if raw_result else (
|
|
262
|
+
f"Agent '{agent_name}' reported an error with no details "
|
|
263
|
+
f"(exit code {proc.returncode})"
|
|
264
|
+
)
|
|
239
265
|
error_type = _classify_error(error_text)
|
|
240
266
|
if error_type == "permission":
|
|
241
267
|
error_text += _permission_hint(agent_name)
|
|
242
268
|
return DispatchResult(
|
|
243
269
|
agent=agent_name,
|
|
244
270
|
success=False,
|
|
245
|
-
result=
|
|
271
|
+
result=str(raw_result) if raw_result else "",
|
|
246
272
|
session_id=data.get("session_id"),
|
|
247
273
|
cost_usd=data.get("total_cost_usd"),
|
|
248
274
|
duration_ms=data.get("duration_ms"),
|
|
@@ -326,11 +352,21 @@ def dispatch_stream(
|
|
|
326
352
|
text=True,
|
|
327
353
|
env=env,
|
|
328
354
|
)
|
|
329
|
-
except
|
|
355
|
+
except FileNotFoundError as e:
|
|
330
356
|
return DispatchResult(
|
|
331
357
|
agent=agent_name, success=False, result="", error=str(e),
|
|
332
358
|
error_type="not_found",
|
|
333
359
|
)
|
|
360
|
+
except PermissionError as e:
|
|
361
|
+
return DispatchResult(
|
|
362
|
+
agent=agent_name, success=False, result="", error=str(e),
|
|
363
|
+
error_type="permission",
|
|
364
|
+
)
|
|
365
|
+
except OSError as e:
|
|
366
|
+
return DispatchResult(
|
|
367
|
+
agent=agent_name, success=False, result="", error=str(e),
|
|
368
|
+
error_type="cli_error",
|
|
369
|
+
)
|
|
334
370
|
|
|
335
371
|
# Kill the process if it exceeds the timeout
|
|
336
372
|
timed_out = threading.Event()
|
|
@@ -387,14 +423,17 @@ def dispatch_stream(
|
|
|
387
423
|
if result_data:
|
|
388
424
|
is_error = result_data.get("is_error", False)
|
|
389
425
|
if is_error:
|
|
390
|
-
|
|
426
|
+
raw_result = result_data.get("result", "")
|
|
427
|
+
error_text = str(raw_result) if raw_result else (
|
|
428
|
+
f"Agent '{agent_name}' reported an error with no details"
|
|
429
|
+
)
|
|
391
430
|
error_type = _classify_error(error_text)
|
|
392
431
|
if error_type == "permission":
|
|
393
432
|
error_text += _permission_hint(agent_name)
|
|
394
433
|
return DispatchResult(
|
|
395
434
|
agent=agent_name,
|
|
396
435
|
success=False,
|
|
397
|
-
result=
|
|
436
|
+
result=str(raw_result) if raw_result else "",
|
|
398
437
|
session_id=result_data.get("session_id"),
|
|
399
438
|
cost_usd=result_data.get("total_cost_usd"),
|
|
400
439
|
duration_ms=result_data.get("duration_ms"),
|
|
@@ -113,9 +113,10 @@ async def list_agents(ctx: Context | None = None) -> str:
|
|
|
113
113
|
}
|
|
114
114
|
if agent.permission_mode:
|
|
115
115
|
entry["permission_mode"] = agent.permission_mode
|
|
116
|
-
|
|
116
|
+
# Include when explicitly set (even []) to distinguish from inheriting defaults
|
|
117
|
+
if agent.allowed_tools is not None:
|
|
117
118
|
entry["allowed_tools"] = agent.allowed_tools
|
|
118
|
-
if agent.disallowed_tools:
|
|
119
|
+
if agent.disallowed_tools is not None:
|
|
119
120
|
entry["disallowed_tools"] = agent.disallowed_tools
|
|
120
121
|
agents.append(entry)
|
|
121
122
|
if ctx:
|
|
@@ -326,6 +327,7 @@ async def dispatch_parallel(
|
|
|
326
327
|
"success": False,
|
|
327
328
|
"result": "",
|
|
328
329
|
"error": str(res),
|
|
330
|
+
"error_type": "cli_error",
|
|
329
331
|
})
|
|
330
332
|
else:
|
|
331
333
|
output.append(res)
|
|
@@ -421,16 +423,22 @@ async def dispatch_stream(
|
|
|
421
423
|
# Forward progress messages while the subprocess runs
|
|
422
424
|
while not future.done():
|
|
423
425
|
await asyncio.sleep(0.1)
|
|
424
|
-
while
|
|
425
|
-
|
|
426
|
+
while True:
|
|
427
|
+
try:
|
|
428
|
+
msg = progress_queue.get_nowait()
|
|
429
|
+
except queue.Empty:
|
|
430
|
+
break
|
|
426
431
|
if ctx:
|
|
427
432
|
await ctx.info(f"[{agent}] {msg[:300]}")
|
|
428
433
|
|
|
429
434
|
result = await asyncio.wrap_future(future)
|
|
430
435
|
|
|
431
436
|
# Drain any remaining messages
|
|
432
|
-
while
|
|
433
|
-
|
|
437
|
+
while True:
|
|
438
|
+
try:
|
|
439
|
+
msg = progress_queue.get_nowait()
|
|
440
|
+
except queue.Empty:
|
|
441
|
+
break
|
|
434
442
|
if ctx:
|
|
435
443
|
await ctx.info(f"[{agent}] {msg[:300]}")
|
|
436
444
|
|
|
@@ -642,7 +650,7 @@ async def add_agent(
|
|
|
642
650
|
directory: Absolute path to the project directory.
|
|
643
651
|
description: What this agent can do. Leave empty for auto-generation.
|
|
644
652
|
timeout: Timeout in seconds (0 uses global default of 300).
|
|
645
|
-
max_budget_usd: Max cost in USD per dispatch (0 = no limit).
|
|
653
|
+
max_budget_usd: Max cost in USD per dispatch (0 or omitted = no limit).
|
|
646
654
|
permission_mode: Permission mode for the claude CLI
|
|
647
655
|
(e.g. default, plan, bypassPermissions). Leave empty for default.
|
|
648
656
|
allowed_tools: Comma-separated list of allowed tools
|
|
@@ -666,8 +674,17 @@ async def add_agent(
|
|
|
666
674
|
return json.dumps({"error": f"Agent '{name}' already exists. Remove it first."})
|
|
667
675
|
|
|
668
676
|
desc = description or auto_describe(dir_path)
|
|
669
|
-
parsed_allowed =
|
|
670
|
-
|
|
677
|
+
parsed_allowed = (
|
|
678
|
+
[t.strip() for t in allowed_tools.split(",") if t.strip()]
|
|
679
|
+
if allowed_tools else None
|
|
680
|
+
)
|
|
681
|
+
parsed_disallowed = (
|
|
682
|
+
[t.strip() for t in disallowed_tools.split(",") if t.strip()]
|
|
683
|
+
if disallowed_tools else None
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
if ctx and (warning := check_permission_mode(permission_mode or None)):
|
|
687
|
+
await ctx.info(f"Warning: {warning}")
|
|
671
688
|
|
|
672
689
|
config.agents[name] = AgentConfig(
|
|
673
690
|
directory=dir_path,
|
|
@@ -681,8 +698,6 @@ async def add_agent(
|
|
|
681
698
|
save_config(config)
|
|
682
699
|
|
|
683
700
|
if ctx:
|
|
684
|
-
if warning := check_permission_mode(permission_mode or None):
|
|
685
|
-
await ctx.info(f"Warning: {warning}")
|
|
686
701
|
await ctx.info(f"Added agent '{name}' -> {dir_path}")
|
|
687
702
|
|
|
688
703
|
result: dict = {
|
|
@@ -746,8 +761,8 @@ async def update_agent(
|
|
|
746
761
|
name: Agent name to update.
|
|
747
762
|
description: New description.
|
|
748
763
|
timeout: New timeout in seconds (0 = don't change).
|
|
749
|
-
max_budget_usd:
|
|
750
|
-
negative
|
|
764
|
+
max_budget_usd: New max cost in USD per dispatch (0 = don't change;
|
|
765
|
+
pass a negative number to clear the limit).
|
|
751
766
|
model: Model override. Pass "none" to clear.
|
|
752
767
|
permission_mode: Permission mode. Pass "none" to clear.
|
|
753
768
|
allowed_tools: Comma-separated allowed tools. Pass "none" to clear.
|
|
@@ -781,13 +796,13 @@ async def update_agent(
|
|
|
781
796
|
updated.append("permission_mode")
|
|
782
797
|
if allowed_tools:
|
|
783
798
|
if allowed_tools.lower() == "none":
|
|
784
|
-
agent.allowed_tools =
|
|
799
|
+
agent.allowed_tools = None
|
|
785
800
|
else:
|
|
786
801
|
agent.allowed_tools = [t.strip() for t in allowed_tools.split(",") if t.strip()]
|
|
787
802
|
updated.append("allowed_tools")
|
|
788
803
|
if disallowed_tools:
|
|
789
804
|
if disallowed_tools.lower() == "none":
|
|
790
|
-
agent.disallowed_tools =
|
|
805
|
+
agent.disallowed_tools = None
|
|
791
806
|
else:
|
|
792
807
|
agent.disallowed_tools = [
|
|
793
808
|
t.strip() for t in disallowed_tools.split(",") if t.strip()
|