deepagent-code 0.1.5__tar.gz → 0.1.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagent-code
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: A Claude Code-style CLI for running LangGraph agents from the terminal
5
5
  Author-email: Kedar Dabhadkar <kdabhadk@gmail.com>
6
6
  License-Expression: MIT
@@ -104,8 +104,36 @@ deepagent-code
104
104
  # Working directory
105
105
  export DEEPAGENT_WORKSPACE_ROOT="/path/to/workspace"
106
106
 
107
- # Configuration
108
- export DEEPAGENT_CONFIG='{"configurable": {"thread_id": "1"}}'
107
+ # Stream mode (updates or values)
108
+ export DEEPAGENT_STREAM_MODE="updates"
109
+ ```
110
+
111
+ ## Configuration Files
112
+
113
+ `deepagent-code` reads TOML config from two locations and merges them
114
+ (project overrides global):
115
+
116
+ - **Global**: `~/.deepagents/config.toml` (shared with the upstream
117
+ `deepagents` CLI)
118
+ - **Project**: `deepagents.toml` in the current directory or any ancestor
119
+
120
+ Precedence: CLI args > env vars > project TOML > global TOML > defaults.
121
+
122
+ Example `deepagents.toml`:
123
+
124
+ ```toml
125
+ [agent]
126
+ spec = "my_agent.py:graph"
127
+ workspace_root = "."
128
+
129
+ [ui]
130
+ verbose = true
131
+ async_mode = false
132
+ stream_mode = "updates"
133
+
134
+ [configurable]
135
+ # seeds LangGraph RunnableConfig.configurable
136
+ thread_id = "my-thread"
109
137
  ```
110
138
 
111
139
  ## CLI Options
@@ -120,9 +148,9 @@ Options:
120
148
  -a, --agent TEXT Agent spec (path/to/file.py:graph or module:graph)
121
149
  -g, --graph-name TEXT Graph variable name (default: "graph")
122
150
  -f, --file PATH Read message from a file (any extension)
123
- -c, --config TEXT Config JSON or file path
124
151
  --interactive/--no-interactive Handle interrupts (default: interactive)
125
152
  --async-mode/--sync-mode Async streaming (default: sync)
153
+ --stream-mode TEXT Stream mode (updates or values)
126
154
  -v, --verbose Verbose output
127
155
  ```
128
156
 
@@ -72,8 +72,36 @@ deepagent-code
72
72
  # Working directory
73
73
  export DEEPAGENT_WORKSPACE_ROOT="/path/to/workspace"
74
74
 
75
- # Configuration
76
- export DEEPAGENT_CONFIG='{"configurable": {"thread_id": "1"}}'
75
+ # Stream mode (updates or values)
76
+ export DEEPAGENT_STREAM_MODE="updates"
77
+ ```
78
+
79
+ ## Configuration Files
80
+
81
+ `deepagent-code` reads TOML config from two locations and merges them
82
+ (project overrides global):
83
+
84
+ - **Global**: `~/.deepagents/config.toml` (shared with the upstream
85
+ `deepagents` CLI)
86
+ - **Project**: `deepagents.toml` in the current directory or any ancestor
87
+
88
+ Precedence: CLI args > env vars > project TOML > global TOML > defaults.
89
+
90
+ Example `deepagents.toml`:
91
+
92
+ ```toml
93
+ [agent]
94
+ spec = "my_agent.py:graph"
95
+ workspace_root = "."
96
+
97
+ [ui]
98
+ verbose = true
99
+ async_mode = false
100
+ stream_mode = "updates"
101
+
102
+ [configurable]
103
+ # seeds LangGraph RunnableConfig.configurable
104
+ thread_id = "my-thread"
77
105
  ```
78
106
 
79
107
  ## CLI Options
@@ -88,9 +116,9 @@ Options:
88
116
  -a, --agent TEXT Agent spec (path/to/file.py:graph or module:graph)
89
117
  -g, --graph-name TEXT Graph variable name (default: "graph")
90
118
  -f, --file PATH Read message from a file (any extension)
91
- -c, --config TEXT Config JSON or file path
92
119
  --interactive/--no-interactive Handle interrupts (default: interactive)
93
120
  --async-mode/--sync-mode Async streaming (default: sync)
121
+ --stream-mode TEXT Stream mode (updates or values)
94
122
  -v, --verbose Verbose output
95
123
  ```
96
124
 
@@ -37,6 +37,7 @@ from deepagent_code.utils import (
37
37
  stream_graph_updates,
38
38
  astream_graph_updates,
39
39
  )
40
+ from deepagent_code import config as config_module
40
41
 
41
42
 
42
43
  # ANSI color codes (matching nanocode style)
@@ -54,7 +55,7 @@ SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇",
54
55
 
55
56
 
56
57
  # Version info
57
- __version__ = "0.1.5"
58
+ __version__ = "0.1.6"
58
59
 
59
60
 
60
61
  # Slash command registry
@@ -1000,40 +1001,50 @@ def cmd_config(args: str, context: Dict[str, Any]) -> Optional[str]:
1000
1001
  config = context.get("config", {})
1001
1002
 
1002
1003
  if not args:
1003
- # Show current config
1004
1004
  print(f"\n{BOLD}{BRIGHT_CYAN}Configuration{RESET}")
1005
1005
  print(f"{DIM}{'─' * 30}{RESET}")
1006
- for key, value in config.items():
1007
- if isinstance(value, dict):
1008
- print(f" {CYAN}{key}:{RESET}")
1009
- for k, v in value.items():
1010
- # Truncate long values
1011
- v_str = str(v)
1012
- if len(v_str) > 30:
1013
- v_str = v_str[:30] + "..."
1014
- print(f" {DIM}{k}:{RESET} {v_str}")
1015
- else:
1016
- print(f" {CYAN}{key}:{RESET} {value}")
1006
+
1007
+ sources = config.get("_toml_sources", [])
1008
+ if sources:
1009
+ print(f" {DIM}TOML sources:{RESET}")
1010
+ for src in sources:
1011
+ print(f" {DIM}- {src}{RESET}")
1012
+ else:
1013
+ print(f" {DIM}TOML sources:{RESET} {DIM}(none — using defaults){RESET}")
1014
+
1015
+ print(f" {DIM}Resolved settings:{RESET}")
1016
+ print(f" {CYAN}verbose:{RESET} {context.get('verbose', False)}")
1017
+ print(f" {CYAN}async_mode:{RESET} {context.get('use_async', False)}")
1018
+ print(f" {CYAN}stream_mode:{RESET} {context.get('stream_mode', 'updates')}")
1019
+
1020
+ configurable = config.get("configurable", {})
1021
+ if configurable:
1022
+ print(f" {DIM}LangGraph configurable:{RESET}")
1023
+ for k, v in configurable.items():
1024
+ v_str = str(v)
1025
+ if len(v_str) > 50:
1026
+ v_str = v_str[:50] + "..."
1027
+ print(f" {CYAN}{k}:{RESET} {v_str}")
1017
1028
  print()
1018
1029
  else:
1019
1030
  parts = args.split(maxsplit=1)
1020
1031
  if len(parts) == 1:
1021
- # Show specific config key
1022
1032
  key = parts[0]
1023
- if key in config:
1024
- print(f"\n{CYAN}{key}:{RESET} {config[key]}\n")
1025
- elif "configurable" in config and key in config["configurable"]:
1026
- print(f"\n{CYAN}{key}:{RESET} {config['configurable'][key]}\n")
1033
+ configurable = config.get("configurable", {})
1034
+ if key in configurable:
1035
+ print(f"\n{CYAN}{key}:{RESET} {configurable[key]}\n")
1036
+ elif key in ("verbose", "async_mode", "stream_mode"):
1037
+ ctx_key = "use_async" if key == "async_mode" else key
1038
+ print(f"\n{CYAN}{key}:{RESET} {context.get(ctx_key)}\n")
1027
1039
  else:
1028
1040
  print(f"{YELLOW}Unknown config key: {key}{RESET}")
1029
1041
  else:
1030
- # Set config value
1031
1042
  key, value = parts
1032
1043
  if key == "verbose":
1033
1044
  context["verbose"] = value.lower() in ("true", "1", "on", "yes")
1034
1045
  print(f"{GREEN}✓ Set verbose = {context['verbose']}{RESET}")
1035
1046
  else:
1036
- print(f"{YELLOW}Cannot modify {key} at runtime{RESET}")
1047
+ print(f"{YELLOW}Cannot modify {key} at runtime (edit deepagents.toml){RESET}")
1037
1048
  return None
1038
1049
 
1039
1050
 
@@ -1348,11 +1359,6 @@ def run_conversation_loop(
1348
1359
  type=click.Path(exists=True),
1349
1360
  help="Read input message from a file (any extension)",
1350
1361
  )
1351
- @click.option(
1352
- "--config",
1353
- "-c",
1354
- help="Configuration JSON string or path to JSON file",
1355
- )
1356
1362
  @click.option(
1357
1363
  "--interactive/--no-interactive",
1358
1364
  default=True,
@@ -1361,7 +1367,7 @@ def run_conversation_loop(
1361
1367
  @click.option(
1362
1368
  "--async-mode/--sync-mode",
1363
1369
  "use_async",
1364
- default=False,
1370
+ default=None,
1365
1371
  help="Use async streaming (default: sync)",
1366
1372
  )
1367
1373
  @click.option(
@@ -1372,6 +1378,7 @@ def run_conversation_loop(
1372
1378
  "--verbose",
1373
1379
  "-v",
1374
1380
  is_flag=True,
1381
+ default=None,
1375
1382
  help="Show verbose output including node names",
1376
1383
  )
1377
1384
  def main(
@@ -1379,11 +1386,10 @@ def main(
1379
1386
  agent_spec: Optional[str],
1380
1387
  graph_name: Optional[str],
1381
1388
  prompt_file: Optional[str],
1382
- config: Optional[str],
1383
1389
  interactive: bool,
1384
- use_async: bool,
1390
+ use_async: Optional[bool],
1385
1391
  stream_mode: Optional[str],
1386
- verbose: bool,
1392
+ verbose: Optional[bool],
1387
1393
  ):
1388
1394
  """
1389
1395
  Run a LangGraph agent from the command line.
@@ -1402,10 +1408,11 @@ def main(
1402
1408
  \b
1403
1409
  - DEEPAGENT_SPEC: Agent location (same formats as above)
1404
1410
  - DEEPAGENT_WORKSPACE_ROOT: Working directory for the agent
1405
- - DEEPAGENT_CONFIG: Configuration JSON string or path to JSON file
1406
1411
  - DEEPAGENT_STREAM_MODE: Stream mode for LangGraph (updates or values)
1407
1412
 
1408
- Command-line arguments override environment variables.
1413
+ Reads ~/.deepagents/config.toml (global) and deepagents.toml (project,
1414
+ walks up from cwd). Precedence: CLI args > env vars > project TOML >
1415
+ global TOML > built-in defaults.
1409
1416
 
1410
1417
  \b
1411
1418
  Examples:
@@ -1431,15 +1438,44 @@ def main(
1431
1438
  print(f"{RED}⏺ Error reading file '{prompt_file}': {e}{RESET}")
1432
1439
  sys.exit(1)
1433
1440
 
1434
- # Get environment variables (DEEPAGENT_SPEC preferred, DEEPAGENT_AGENT_SPEC for backwards compat)
1435
- env_agent_spec = os.getenv('DEEPAGENT_SPEC') or os.getenv('DEEPAGENT_AGENT_SPEC')
1436
- env_workspace_root = os.getenv('DEEPAGENT_WORKSPACE_ROOT')
1437
- env_config = os.getenv('DEEPAGENT_CONFIG')
1438
- env_stream_mode = os.getenv('DEEPAGENT_STREAM_MODE', 'updates')
1441
+ # Load TOML configuration (global + project, merged)
1442
+ try:
1443
+ toml_config, toml_sources = config_module.load_config()
1444
+ except config_module.ConfigError as e:
1445
+ print(f"{RED}⏺ {e}{RESET}")
1446
+ sys.exit(1)
1439
1447
 
1440
- # Determine which spec to use (CLI arg > env var > default)
1441
- final_spec = agent_spec or env_agent_spec
1442
- default_graph_name = graph_name or "graph"
1448
+ # Resolve settings with precedence: CLI > env > TOML > default
1449
+ final_spec = config_module.resolve(
1450
+ toml_config, "agent.spec",
1451
+ cli_value=agent_spec,
1452
+ env_var="DEEPAGENT_SPEC",
1453
+ ) or os.getenv("DEEPAGENT_AGENT_SPEC") # legacy env var
1454
+ final_graph_name_default = config_module.resolve(
1455
+ toml_config, "agent.graph_name",
1456
+ cli_value=graph_name,
1457
+ default="graph",
1458
+ )
1459
+ workspace_root = config_module.resolve(
1460
+ toml_config, "agent.workspace_root",
1461
+ env_var="DEEPAGENT_WORKSPACE_ROOT",
1462
+ )
1463
+ final_stream_mode = config_module.resolve(
1464
+ toml_config, "ui.stream_mode",
1465
+ cli_value=stream_mode,
1466
+ env_var="DEEPAGENT_STREAM_MODE",
1467
+ default="updates",
1468
+ )
1469
+ use_async = config_module.resolve(
1470
+ toml_config, "ui.async_mode",
1471
+ cli_value=use_async,
1472
+ default=False,
1473
+ )
1474
+ verbose = config_module.resolve(
1475
+ toml_config, "ui.verbose",
1476
+ cli_value=verbose,
1477
+ default=False,
1478
+ )
1443
1479
 
1444
1480
  # If no spec provided, try the default agent
1445
1481
  if not final_spec:
@@ -1455,45 +1491,29 @@ def main(
1455
1491
  sys.exit(1)
1456
1492
 
1457
1493
  # Change to workspace root if specified
1458
- if env_workspace_root:
1459
- workspace_path = Path(env_workspace_root).resolve()
1494
+ if workspace_root:
1495
+ workspace_path = Path(workspace_root).expanduser().resolve()
1460
1496
  if workspace_path.exists():
1461
1497
  os.chdir(workspace_path)
1462
1498
 
1463
1499
  # Load the graph with a spinner
1464
1500
  spinner = Spinner("Loading agent")
1465
1501
  spinner.start()
1466
- graph, final_graph_name = load_graph(final_spec, default_graph_name)
1502
+ graph, final_graph_name = load_graph(final_spec, final_graph_name_default)
1467
1503
  spinner.stop()
1468
1504
  print(f"{GREEN}✓{RESET} {DIM}Loaded {final_spec}{RESET}")
1469
1505
 
1470
- # Parse config
1471
- config_dict = None
1472
- config_source = config or env_config
1473
-
1474
- if config_source:
1475
- config_path = Path(config_source)
1476
- if config_path.exists():
1477
- with open(config_path) as f:
1478
- config_dict = json.load(f)
1479
- else:
1480
- try:
1481
- config_dict = json.loads(config_source)
1482
- except json.JSONDecodeError as e:
1483
- print(f"{RED}⏺ Invalid config JSON: {e}{RESET}")
1484
- sys.exit(1)
1485
-
1486
- # Get stream mode
1487
- final_stream_mode = stream_mode or env_stream_mode
1488
-
1489
- # Ensure config has a thread_id for checkpointer support
1490
- if config_dict is None:
1491
- config_dict = {}
1492
- if "configurable" not in config_dict:
1493
- config_dict["configurable"] = {}
1506
+ # Seed LangGraph RunnableConfig from TOML [configurable] table if present
1507
+ config_dict: Dict[str, Any] = {"configurable": {}}
1508
+ toml_configurable = config_module.get(toml_config, "configurable")
1509
+ if isinstance(toml_configurable, dict):
1510
+ config_dict["configurable"].update(toml_configurable)
1494
1511
  if "thread_id" not in config_dict["configurable"]:
1495
1512
  config_dict["configurable"]["thread_id"] = str(uuid.uuid4())
1496
1513
 
1514
+ # Expose TOML sources to slash commands via the config dict
1515
+ config_dict["_toml_sources"] = [str(p) for p in toml_sources]
1516
+
1497
1517
  # Extract agent name and description from graph object
1498
1518
  agent_name = get_agent_name(graph)
1499
1519
  agent_description = get_agent_description(graph)
@@ -0,0 +1,113 @@
1
+ """TOML configuration loader for deepagent-code.
2
+
3
+ Reads two files and merges them (project wins on conflict):
4
+ - ~/.deepagents/config.toml — shared with the upstream deepagents CLI
5
+ - deepagents.toml — nearest ancestor of cwd, per-project overrides
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import tomllib
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional, Tuple
13
+
14
+
15
+ GLOBAL_CONFIG_PATH = Path.home() / ".deepagents" / "config.toml"
16
+ PROJECT_CONFIG_NAME = "deepagents.toml"
17
+
18
+
19
+ class ConfigError(Exception):
20
+ """Raised when a config file exists but cannot be parsed."""
21
+
22
+
23
+ def global_config_path() -> Path:
24
+ override = os.getenv("DEEPAGENTS_CONFIG_HOME")
25
+ if override:
26
+ return Path(override).expanduser() / "config.toml"
27
+ return GLOBAL_CONFIG_PATH
28
+
29
+
30
+ def find_project_config(start: Optional[Path] = None) -> Optional[Path]:
31
+ """Walk up from `start` (or cwd) looking for deepagents.toml."""
32
+ here = (start or Path.cwd()).resolve()
33
+ for directory in (here, *here.parents):
34
+ candidate = directory / PROJECT_CONFIG_NAME
35
+ if candidate.is_file():
36
+ return candidate
37
+ return None
38
+
39
+
40
+ def _read_toml(path: Path) -> Dict[str, Any]:
41
+ try:
42
+ with path.open("rb") as f:
43
+ return tomllib.load(f)
44
+ except tomllib.TOMLDecodeError as e:
45
+ raise ConfigError(f"Invalid TOML in {path}: {e}") from e
46
+
47
+
48
+ def _deep_merge(base: Dict[str, Any], overlay: Dict[str, Any]) -> Dict[str, Any]:
49
+ """Recursively merge overlay into base. Overlay wins on leaf conflicts."""
50
+ result = dict(base)
51
+ for key, value in overlay.items():
52
+ if (
53
+ key in result
54
+ and isinstance(result[key], dict)
55
+ and isinstance(value, dict)
56
+ ):
57
+ result[key] = _deep_merge(result[key], value)
58
+ else:
59
+ result[key] = value
60
+ return result
61
+
62
+
63
+ def load_config(start: Optional[Path] = None) -> Tuple[Dict[str, Any], List[Path]]:
64
+ """Load global + project TOML, merged. Returns (config, sources_used)."""
65
+ sources: List[Path] = []
66
+ merged: Dict[str, Any] = {}
67
+
68
+ gpath = global_config_path()
69
+ if gpath.exists():
70
+ merged = _deep_merge(merged, _read_toml(gpath))
71
+ sources.append(gpath)
72
+
73
+ ppath = find_project_config(start)
74
+ if ppath is not None:
75
+ merged = _deep_merge(merged, _read_toml(ppath))
76
+ sources.append(ppath)
77
+
78
+ return merged, sources
79
+
80
+
81
+ def get(config: Dict[str, Any], dotted_key: str, default: Any = None) -> Any:
82
+ """Fetch a nested value via dotted path, e.g. 'ui.verbose'."""
83
+ node: Any = config
84
+ for part in dotted_key.split("."):
85
+ if not isinstance(node, dict) or part not in node:
86
+ return default
87
+ node = node[part]
88
+ return node
89
+
90
+
91
+ def resolve(
92
+ config: Dict[str, Any],
93
+ dotted_key: str,
94
+ cli_value: Any = None,
95
+ env_var: Optional[str] = None,
96
+ default: Any = None,
97
+ cast: Optional[type] = None,
98
+ ) -> Any:
99
+ """Resolve a value with precedence: CLI > env > TOML > default."""
100
+ if cli_value is not None:
101
+ return cli_value
102
+ if env_var:
103
+ env_value = os.getenv(env_var)
104
+ if env_value is not None:
105
+ if cast is bool:
106
+ return env_value.lower() in ("1", "true", "yes", "on")
107
+ if cast is not None:
108
+ return cast(env_value)
109
+ return env_value
110
+ toml_value = get(config, dotted_key)
111
+ if toml_value is not None:
112
+ return toml_value
113
+ return default
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagent-code
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: A Claude Code-style CLI for running LangGraph agents from the terminal
5
5
  Author-email: Kedar Dabhadkar <kdabhadk@gmail.com>
6
6
  License-Expression: MIT
@@ -104,8 +104,36 @@ deepagent-code
104
104
  # Working directory
105
105
  export DEEPAGENT_WORKSPACE_ROOT="/path/to/workspace"
106
106
 
107
- # Configuration
108
- export DEEPAGENT_CONFIG='{"configurable": {"thread_id": "1"}}'
107
+ # Stream mode (updates or values)
108
+ export DEEPAGENT_STREAM_MODE="updates"
109
+ ```
110
+
111
+ ## Configuration Files
112
+
113
+ `deepagent-code` reads TOML config from two locations and merges them
114
+ (project overrides global):
115
+
116
+ - **Global**: `~/.deepagents/config.toml` (shared with the upstream
117
+ `deepagents` CLI)
118
+ - **Project**: `deepagents.toml` in the current directory or any ancestor
119
+
120
+ Precedence: CLI args > env vars > project TOML > global TOML > defaults.
121
+
122
+ Example `deepagents.toml`:
123
+
124
+ ```toml
125
+ [agent]
126
+ spec = "my_agent.py:graph"
127
+ workspace_root = "."
128
+
129
+ [ui]
130
+ verbose = true
131
+ async_mode = false
132
+ stream_mode = "updates"
133
+
134
+ [configurable]
135
+ # seeds LangGraph RunnableConfig.configurable
136
+ thread_id = "my-thread"
109
137
  ```
110
138
 
111
139
  ## CLI Options
@@ -120,9 +148,9 @@ Options:
120
148
  -a, --agent TEXT Agent spec (path/to/file.py:graph or module:graph)
121
149
  -g, --graph-name TEXT Graph variable name (default: "graph")
122
150
  -f, --file PATH Read message from a file (any extension)
123
- -c, --config TEXT Config JSON or file path
124
151
  --interactive/--no-interactive Handle interrupts (default: interactive)
125
152
  --async-mode/--sync-mode Async streaming (default: sync)
153
+ --stream-mode TEXT Stream mode (updates or values)
126
154
  -v, --verbose Verbose output
127
155
  ```
128
156
 
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  deepagent_code/__init__.py
5
5
  deepagent_code/cli.py
6
+ deepagent_code/config.py
6
7
  deepagent_code/utils.py
7
8
  deepagent_code.egg-info/PKG-INFO
8
9
  deepagent_code.egg-info/SOURCES.txt
@@ -11,4 +12,5 @@ deepagent_code.egg-info/entry_points.txt
11
12
  deepagent_code.egg-info/requires.txt
12
13
  deepagent_code.egg-info/top_level.txt
13
14
  tests/test_cli.py
15
+ tests/test_config.py
14
16
  tests/test_utils.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepagent-code"
7
- version = "0.1.5"
7
+ version = "0.1.6"
8
8
  description = "A Claude Code-style CLI for running LangGraph agents from the terminal"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,113 @@
1
+ """Tests for deepagent_code.config."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from deepagent_code import config
9
+
10
+
11
+ def _write(path: Path, content: str) -> None:
12
+ path.parent.mkdir(parents=True, exist_ok=True)
13
+ path.write_text(content)
14
+
15
+
16
+ def test_returns_empty_when_no_files(tmp_path, monkeypatch):
17
+ monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(tmp_path / "empty"))
18
+ monkeypatch.chdir(tmp_path)
19
+ cfg, sources = config.load_config()
20
+ assert cfg == {}
21
+ assert sources == []
22
+
23
+
24
+ def test_loads_global_only(tmp_path, monkeypatch):
25
+ home = tmp_path / "home"
26
+ _write(home / "config.toml", '[ui]\nverbose = true\n')
27
+ monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(home))
28
+ monkeypatch.chdir(tmp_path)
29
+ cfg, sources = config.load_config()
30
+ assert cfg == {"ui": {"verbose": True}}
31
+ assert sources == [home / "config.toml"]
32
+
33
+
34
+ def test_loads_project_only(tmp_path, monkeypatch):
35
+ monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(tmp_path / "empty"))
36
+ project = tmp_path / "proj"
37
+ _write(project / "deepagents.toml", '[agent]\nspec = "foo.py:agent"\n')
38
+ monkeypatch.chdir(project)
39
+ cfg, sources = config.load_config()
40
+ assert cfg == {"agent": {"spec": "foo.py:agent"}}
41
+ assert sources == [project / "deepagents.toml"]
42
+
43
+
44
+ def test_project_overrides_global(tmp_path, monkeypatch):
45
+ home = tmp_path / "home"
46
+ _write(home / "config.toml", '[ui]\nverbose = false\nstream_mode = "values"\n')
47
+ monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(home))
48
+
49
+ project = tmp_path / "proj"
50
+ _write(project / "deepagents.toml", '[ui]\nverbose = true\n')
51
+ monkeypatch.chdir(project)
52
+
53
+ cfg, sources = config.load_config()
54
+ # project wins on overlapping keys, global keys still present
55
+ assert cfg == {"ui": {"verbose": True, "stream_mode": "values"}}
56
+ assert sources == [home / "config.toml", project / "deepagents.toml"]
57
+
58
+
59
+ def test_walks_up_for_project_config(tmp_path, monkeypatch):
60
+ monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(tmp_path / "empty"))
61
+ project = tmp_path / "proj"
62
+ nested = project / "a" / "b" / "c"
63
+ nested.mkdir(parents=True)
64
+ _write(project / "deepagents.toml", '[ui]\nverbose = true\n')
65
+ monkeypatch.chdir(nested)
66
+
67
+ cfg, sources = config.load_config()
68
+ assert cfg == {"ui": {"verbose": True}}
69
+ assert sources == [project / "deepagents.toml"]
70
+
71
+
72
+ def test_invalid_toml_raises(tmp_path, monkeypatch):
73
+ home = tmp_path / "home"
74
+ _write(home / "config.toml", "this is = not = valid toml\n")
75
+ monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(home))
76
+ monkeypatch.chdir(tmp_path)
77
+ with pytest.raises(config.ConfigError):
78
+ config.load_config()
79
+
80
+
81
+ def test_get_dotted_path():
82
+ data = {"ui": {"verbose": True}, "agent": {"spec": "x"}}
83
+ assert config.get(data, "ui.verbose") is True
84
+ assert config.get(data, "agent.spec") == "x"
85
+ assert config.get(data, "missing") is None
86
+ assert config.get(data, "ui.missing", default="d") == "d"
87
+ assert config.get(data, "ui.verbose.nested", default="d") == "d"
88
+
89
+
90
+ def test_resolve_precedence(monkeypatch):
91
+ cfg = {"ui": {"verbose": True}}
92
+ monkeypatch.delenv("UI_VERBOSE", raising=False)
93
+
94
+ # default only
95
+ assert config.resolve({}, "ui.verbose", default=False) is False
96
+ # toml beats default
97
+ assert config.resolve(cfg, "ui.verbose", default=False) is True
98
+ # env beats toml
99
+ monkeypatch.setenv("UI_VERBOSE", "false")
100
+ assert config.resolve(cfg, "ui.verbose", env_var="UI_VERBOSE", cast=bool) is False
101
+ # cli beats env
102
+ assert config.resolve(
103
+ cfg, "ui.verbose", cli_value=True, env_var="UI_VERBOSE", cast=bool
104
+ ) is True
105
+
106
+
107
+ def test_resolve_bool_cast(monkeypatch):
108
+ for truthy in ("1", "true", "yes", "on", "TRUE"):
109
+ monkeypatch.setenv("X", truthy)
110
+ assert config.resolve({}, "missing", env_var="X", cast=bool) is True
111
+ for falsy in ("0", "false", "no", "off", ""):
112
+ monkeypatch.setenv("X", falsy)
113
+ assert config.resolve({}, "missing", env_var="X", cast=bool) is False
File without changes
File without changes