nonoka-cli 0.1.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 (32) hide show
  1. nonoka_cli-0.1.0/.gitignore +49 -0
  2. nonoka_cli-0.1.0/PKG-INFO +69 -0
  3. nonoka_cli-0.1.0/README.md +46 -0
  4. nonoka_cli-0.1.0/config.yaml.example +40 -0
  5. nonoka_cli-0.1.0/pyproject.toml +52 -0
  6. nonoka_cli-0.1.0/src/nonoka_cli/__init__.py +5 -0
  7. nonoka_cli-0.1.0/src/nonoka_cli/__main__.py +7 -0
  8. nonoka_cli-0.1.0/src/nonoka_cli/cli.py +134 -0
  9. nonoka_cli-0.1.0/src/nonoka_cli/config/__init__.py +8 -0
  10. nonoka_cli-0.1.0/src/nonoka_cli/config/loader.py +130 -0
  11. nonoka_cli-0.1.0/src/nonoka_cli/config/models.py +51 -0
  12. nonoka_cli-0.1.0/src/nonoka_cli/core/__init__.py +9 -0
  13. nonoka_cli-0.1.0/src/nonoka_cli/core/agent_factory.py +78 -0
  14. nonoka_cli-0.1.0/src/nonoka_cli/core/context.py +17 -0
  15. nonoka_cli-0.1.0/src/nonoka_cli/core/orchestrator.py +138 -0
  16. nonoka_cli-0.1.0/src/nonoka_cli/shell/__init__.py +5 -0
  17. nonoka_cli-0.1.0/src/nonoka_cli/shell/repl.py +164 -0
  18. nonoka_cli-0.1.0/src/nonoka_cli/ui/__init__.py +5 -0
  19. nonoka_cli-0.1.0/src/nonoka_cli/ui/renderer.py +78 -0
  20. nonoka_cli-0.1.0/src/nonoka_cli/utils/__init__.py +1 -0
  21. nonoka_cli-0.1.0/src/nonoka_cli/utils/errors.py +28 -0
  22. nonoka_cli-0.1.0/src/nonoka_cli/utils/logging.py +75 -0
  23. nonoka_cli-0.1.0/tests/__init__.py +1 -0
  24. nonoka_cli-0.1.0/tests/conftest.py +72 -0
  25. nonoka_cli-0.1.0/tests/integration/__init__.py +1 -0
  26. nonoka_cli-0.1.0/tests/integration/test_real_llm.py +215 -0
  27. nonoka_cli-0.1.0/tests/unit/__init__.py +1 -0
  28. nonoka_cli-0.1.0/tests/unit/test_config.py +151 -0
  29. nonoka_cli-0.1.0/tests/unit/test_core.py +193 -0
  30. nonoka_cli-0.1.0/tests/unit/test_shell.py +158 -0
  31. nonoka_cli-0.1.0/tests/unit/test_ui.py +164 -0
  32. nonoka_cli-0.1.0/uv.lock +3268 -0
@@ -0,0 +1,49 @@
1
+ # IDE / Editor
2
+ self-files/
3
+ .claude/
4
+ .idea/
5
+ .vscode/
6
+ *.swp
7
+ *.swo
8
+ *~
9
+
10
+ # Environment
11
+ .env
12
+ .venv/
13
+ venv/
14
+
15
+ # Python
16
+ __pycache__/
17
+ *.py[cod]
18
+ *$py.class
19
+ *.so
20
+ .Python
21
+ build/
22
+ develop-eggs/
23
+ dist/
24
+ downloads/
25
+ eggs/
26
+ .eggs/
27
+ lib/
28
+ lib64/
29
+ parts/
30
+ sdist/
31
+ var/
32
+ wheels/
33
+ *.egg-info/
34
+ .installed.cfg
35
+ *.egg
36
+ MANIFEST
37
+
38
+ # Testing
39
+ .pytest_cache/
40
+ .coverage
41
+ htmlcov/
42
+ .tox/
43
+
44
+ # OS
45
+ .DS_Store
46
+ Thumbs.db
47
+
48
+ # Logs
49
+ *.log
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: nonoka-cli
3
+ Version: 0.1.0
4
+ Summary: Terminal frontend for the Nonoka Agent framework
5
+ License: MIT
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: nonoka>=1.1.7
16
+ Requires-Dist: rich>=13.0.0
17
+ Requires-Dist: structlog>=24.0.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
20
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
21
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # nonoka-cli
25
+
26
+ Terminal frontend for the [Nonoka](https://pypi.org/project/nonoka) Agent framework.
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ # Install
32
+ pip install nonoka-cli
33
+
34
+ # Start interactive REPL
35
+ nonoka-cli
36
+
37
+ # With custom config
38
+ nonoka-cli --config ~/.config/nonoka/config.yaml
39
+
40
+ # Override model
41
+ nonoka-cli --model gpt-4o
42
+ ```
43
+
44
+ ## Configuration
45
+
46
+ Create `~/.config/nonoka/config.yaml`:
47
+
48
+ ```yaml
49
+ model: "gpt-4o"
50
+ system_prompt: |
51
+ You are a helpful AI assistant.
52
+ ```
53
+
54
+ ## Commands
55
+
56
+ - `/exit` — Quit the CLI
57
+ - `/new` — Start a new session
58
+ - `/help` — Show help
59
+
60
+ ## Development
61
+
62
+ ```bash
63
+ # Setup
64
+ uv venv
65
+ uv pip install -e ".[dev]"
66
+
67
+ # Run tests
68
+ pytest
69
+ ```
@@ -0,0 +1,46 @@
1
+ # nonoka-cli
2
+
3
+ Terminal frontend for the [Nonoka](https://pypi.org/project/nonoka) Agent framework.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install
9
+ pip install nonoka-cli
10
+
11
+ # Start interactive REPL
12
+ nonoka-cli
13
+
14
+ # With custom config
15
+ nonoka-cli --config ~/.config/nonoka/config.yaml
16
+
17
+ # Override model
18
+ nonoka-cli --model gpt-4o
19
+ ```
20
+
21
+ ## Configuration
22
+
23
+ Create `~/.config/nonoka/config.yaml`:
24
+
25
+ ```yaml
26
+ model: "gpt-4o"
27
+ system_prompt: |
28
+ You are a helpful AI assistant.
29
+ ```
30
+
31
+ ## Commands
32
+
33
+ - `/exit` — Quit the CLI
34
+ - `/new` — Start a new session
35
+ - `/help` — Show help
36
+
37
+ ## Development
38
+
39
+ ```bash
40
+ # Setup
41
+ uv venv
42
+ uv pip install -e ".[dev]"
43
+
44
+ # Run tests
45
+ pytest
46
+ ```
@@ -0,0 +1,40 @@
1
+ # nonoka-cli configuration example — Deepseek API
2
+ # Copy to ~/.config/nonoka/config.yaml to use
3
+
4
+ model: "deepseek-chat"
5
+
6
+ system_prompt: |
7
+ You are nonoka, a helpful AI assistant running in a terminal.
8
+ Be concise but thorough in your responses.
9
+
10
+ # TODO: MCP server configuration (enabled in Phase 5)
11
+ # mcp_servers:
12
+ # filesystem:
13
+ # transport: stdio
14
+ # command: npx
15
+ # args:
16
+ # - "-y"
17
+ # - "@modelcontextprotocol/server-filesystem"
18
+ # - "~"
19
+
20
+ # TODO: Local tool scan paths (enabled in Phase 6)
21
+ # tool_paths:
22
+ # - ~/.config/nonoka/tools/
23
+
24
+ # TODO: Enabled skills (enabled in Phase 6)
25
+ # skills:
26
+ # - code-review
27
+
28
+ cli:
29
+ theme: dark
30
+ auto_approve: false
31
+ editor: "${EDITOR:-nano}"
32
+ max_history: 1000
33
+
34
+ hitl:
35
+ policy: interactive
36
+ dangerous_tools:
37
+ - write_file
38
+ - edit_file
39
+ - delete_file
40
+ - execute_command
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "nonoka-cli"
7
+ version = "0.1.0"
8
+ description = "Terminal frontend for the Nonoka Agent framework"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ ]
22
+ dependencies = [
23
+ "nonoka>=1.1.7",
24
+ "rich>=13.0.0",
25
+ "structlog>=24.0.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ nonoka-cli = "nonoka_cli.cli:main"
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=8.0.0",
34
+ "pytest-asyncio>=0.23.0",
35
+ "pytest-cov>=5.0.0",
36
+ ]
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/nonoka_cli"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ pythonpath = ["src"]
44
+ asyncio_mode = "auto"
45
+ addopts = "-v --tb=short"
46
+
47
+ [tool.coverage.run]
48
+ source = ["src/nonoka_cli"]
49
+
50
+ [tool.ruff]
51
+ line-length = 100
52
+ select = ["E", "F", "W", "I"]
@@ -0,0 +1,5 @@
1
+ """Nonoka CLI — Terminal frontend for the Nonoka Agent framework."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ """Allow running nonoka-cli as a module: python -m nonoka_cli."""
2
+
3
+ from nonoka_cli.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ import sys
7
+ sys.exit(main())
@@ -0,0 +1,134 @@
1
+ """CLI entry point for nonoka-cli.
2
+
3
+ Handles argument parsing, configuration loading, and REPL startup.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import asyncio
10
+ import logging
11
+ import signal
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ import structlog
16
+
17
+ from nonoka_cli.core.orchestrator import Orchestrator
18
+ from nonoka_cli.shell.repl import REPL
19
+ from nonoka_cli.ui.renderer import Renderer
20
+ from nonoka_cli.utils.logging import setup_logging
21
+
22
+ logger = structlog.get_logger("nonoka_cli.cli")
23
+
24
+
25
+ def _build_parser() -> argparse.ArgumentParser:
26
+ """Build the argument parser."""
27
+ parser = argparse.ArgumentParser(
28
+ prog="nonoka-cli",
29
+ description="Terminal frontend for the Nonoka Agent framework",
30
+ )
31
+ parser.add_argument(
32
+ "--config",
33
+ type=Path,
34
+ default=None,
35
+ help="Path to configuration file (default: ~/.config/nonoka/config.yaml)",
36
+ )
37
+ parser.add_argument(
38
+ "--model",
39
+ type=str,
40
+ default=None,
41
+ help="Override the model specified in config",
42
+ )
43
+ parser.add_argument(
44
+ "--verbose", "-v",
45
+ action="store_true",
46
+ help="Output INFO level logs to console",
47
+ )
48
+ parser.add_argument(
49
+ "--debug",
50
+ action="store_true",
51
+ help="Output DEBUG level logs to console",
52
+ )
53
+ return parser
54
+
55
+
56
+ async def _run_repl(args: argparse.Namespace) -> int:
57
+ """Initialize and run the REPL.
58
+
59
+ Args:
60
+ args: Parsed command-line arguments.
61
+
62
+ Returns:
63
+ Exit code (0 for success).
64
+ """
65
+ orchestrator = Orchestrator()
66
+ renderer = Renderer()
67
+ repl = REPL(orchestrator, renderer)
68
+
69
+ # Handle Ctrl+C by interrupting the current execution
70
+ loop = asyncio.get_event_loop()
71
+
72
+ def _sigint_handler():
73
+ asyncio.create_task(repl.interrupt())
74
+
75
+ if sys.platform != "win32":
76
+ loop.add_signal_handler(signal.SIGINT, _sigint_handler)
77
+
78
+ try:
79
+ await orchestrator.initialize(config_path=args.config)
80
+ except Exception as exc:
81
+ print(f"Failed to initialize: {exc}", file=sys.stderr)
82
+ logger.error("initialization_failed", error=str(exc))
83
+ return 1
84
+
85
+ # Apply model override if provided
86
+ if args.model:
87
+ orchestrator.config.model = args.model
88
+ orchestrator._agent_factory.rebuild()
89
+ logger.info("model_overridden", model=args.model)
90
+
91
+ print(f"nonoka-cli initialized. Model: {orchestrator.config.model}")
92
+ print("Type /help for commands, /exit to quit.")
93
+ print()
94
+
95
+ try:
96
+ await repl.run()
97
+ finally:
98
+ await orchestrator.shutdown()
99
+ if sys.platform != "win32":
100
+ loop.remove_signal_handler(signal.SIGINT)
101
+
102
+ return 0
103
+
104
+
105
+ def main() -> int:
106
+ """Main entry point.
107
+
108
+ Returns:
109
+ Exit code (0 for success, 1 for error).
110
+ """
111
+ parser = _build_parser()
112
+ args = parser.parse_args()
113
+
114
+ # Setup logging
115
+ log_level = (
116
+ logging.DEBUG if args.debug
117
+ else logging.INFO if args.verbose
118
+ else logging.WARNING
119
+ )
120
+ setup_logging(level=log_level, console=args.verbose or args.debug)
121
+
122
+ try:
123
+ return asyncio.run(_run_repl(args))
124
+ except KeyboardInterrupt:
125
+ print("\nInterrupted.")
126
+ return 130
127
+ except Exception as exc:
128
+ logger.error("fatal_error", error=str(exc))
129
+ print(f"Fatal error: {exc}", file=sys.stderr)
130
+ return 1
131
+
132
+
133
+ if __name__ == "__main__":
134
+ sys.exit(main())
@@ -0,0 +1,8 @@
1
+ """Configuration loading and validation for nonoka-cli."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from nonoka_cli.config.models import CLIConfig
6
+ from nonoka_cli.config.loader import ConfigLoader, load_config
7
+
8
+ __all__ = ["CLIConfig", "ConfigLoader", "load_config"]
@@ -0,0 +1,130 @@
1
+ """Configuration loading with env-var substitution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import structlog
11
+
12
+ from nonoka_cli.config.models import CLIConfig
13
+ from nonoka_cli.utils.errors import ConfigError, ConfigNotFoundError
14
+
15
+ logger = structlog.get_logger("nonoka_cli.config")
16
+
17
+ _ENV_PATTERN = re.compile(r"\$\{(?P<name>[A-Za-z_][A-Za-z0-9_]*)(?::-?(?P<default>[^}]*))?\}")
18
+
19
+
20
+ def _substitute_env_vars(value: Any) -> Any:
21
+ """Recursively substitute ``${VAR}`` and ``${VAR:-default}`` in strings."""
22
+ if isinstance(value, str):
23
+ def replacer(match: re.Match[str]) -> str:
24
+ var_name = match.group("name")
25
+ default = match.group("default")
26
+ result = os.getenv(var_name)
27
+ if result is None:
28
+ if default is not None:
29
+ return default
30
+ raise ConfigError(
31
+ f"Environment variable '{var_name}' is not set and no default provided"
32
+ )
33
+ return result
34
+ return _ENV_PATTERN.sub(replacer, value)
35
+ if isinstance(value, dict):
36
+ return {k: _substitute_env_vars(v) for k, v in value.items()}
37
+ if isinstance(value, list):
38
+ return [_substitute_env_vars(item) for item in value]
39
+ return value
40
+
41
+
42
+ class ConfigLoader:
43
+ """Loads and validates CLI configuration from YAML files."""
44
+
45
+ DEFAULT_PATH = Path.home() / ".config" / "nonoka" / "config.yaml"
46
+ FALLBACK_PATH = Path.cwd() / "nonoka.yaml"
47
+
48
+ @classmethod
49
+ def find_config_file(
50
+ cls,
51
+ explicit_path: Path | str | None = None,
52
+ ) -> Path:
53
+ """Search for a configuration file in priority order.
54
+
55
+ Priority: explicit_path > ~/.config/nonoka/config.yaml > ./nonoka.yaml
56
+ """
57
+ if explicit_path is not None:
58
+ path = Path(explicit_path)
59
+ if path.exists():
60
+ return path
61
+ raise ConfigNotFoundError(f"Explicit config file not found: {path}")
62
+
63
+ if cls.DEFAULT_PATH.exists():
64
+ return cls.DEFAULT_PATH
65
+
66
+ if cls.FALLBACK_PATH.exists():
67
+ return cls.FALLBACK_PATH
68
+
69
+ raise ConfigNotFoundError(
70
+ f"No config file found. Searched: {cls.DEFAULT_PATH}, {cls.FALLBACK_PATH}"
71
+ )
72
+
73
+ @classmethod
74
+ def load(
75
+ cls,
76
+ path: Path | str | None = None,
77
+ ) -> CLIConfig:
78
+ """Load configuration from a YAML file.
79
+
80
+ Args:
81
+ path: Explicit config file path. If None, searches default locations.
82
+
83
+ Returns:
84
+ Validated CLIConfig instance.
85
+
86
+ Raises:
87
+ ConfigNotFoundError: If no config file is found.
88
+ ConfigError: If parsing or validation fails.
89
+ """
90
+ config_path = cls.find_config_file(path)
91
+ logger.info("loading_config", path=str(config_path))
92
+
93
+ try:
94
+ import yaml
95
+ except ImportError as exc:
96
+ raise ConfigError("PyYAML is required. Install: pip install pyyaml") from exc
97
+
98
+ try:
99
+ raw = config_path.read_text(encoding="utf-8")
100
+ data = yaml.safe_load(raw)
101
+ except Exception as exc:
102
+ raise ConfigError(f"Failed to parse YAML: {exc}") from exc
103
+
104
+ if data is None:
105
+ data = {}
106
+ if not isinstance(data, dict):
107
+ raise ConfigError(
108
+ f"Config file must contain a top-level object, got {type(data).__name__}"
109
+ )
110
+
111
+ # Substitute env vars before validation
112
+ try:
113
+ data = _substitute_env_vars(data)
114
+ except ConfigError:
115
+ raise
116
+ except Exception as exc:
117
+ raise ConfigError(f"Environment variable substitution failed: {exc}") from exc
118
+
119
+ try:
120
+ config = CLIConfig.model_validate(data)
121
+ except Exception as exc:
122
+ raise ConfigError(f"Config validation failed: {exc}") from exc
123
+
124
+ logger.info("config_loaded", model=config.model)
125
+ return config
126
+
127
+
128
+ def load_config(path: Path | str | None = None) -> CLIConfig:
129
+ """Convenience function: load CLI configuration."""
130
+ return ConfigLoader.load(path)
@@ -0,0 +1,51 @@
1
+ """Pydantic models for CLI configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field, field_validator
9
+
10
+
11
+ class CLIConfigModel(BaseModel):
12
+ """CLI behavior configuration."""
13
+ theme: str = "dark"
14
+ auto_approve: bool = False
15
+ editor: str = "${EDITOR:-nano}"
16
+ max_history: int = 1000
17
+ multi_line_trigger: str = '"""'
18
+
19
+
20
+ class HITLConfigModel(BaseModel):
21
+ """HITL (Human-in-the-Loop) configuration."""
22
+ policy: str = "interactive"
23
+ dangerous_tools: list[str] = Field(default_factory=list)
24
+
25
+
26
+ class MCPServerConfigModel(BaseModel):
27
+ """Single MCP server configuration."""
28
+ transport: str
29
+ command: str
30
+ args: list[str] = Field(default_factory=list)
31
+
32
+
33
+ class CLIConfig(BaseModel):
34
+ """Top-level configuration for nonoka-cli.
35
+
36
+ Matches the expected structure of ~/.config/nonoka/config.yaml.
37
+ """
38
+ model: str = "gpt-4o"
39
+ system_prompt: str = "You are a helpful AI assistant."
40
+ mcp_servers: dict[str, MCPServerConfigModel] = Field(default_factory=dict)
41
+ tool_paths: list[Path] = Field(default_factory=list)
42
+ skills: list[str] = Field(default_factory=list)
43
+ cli: CLIConfigModel = Field(default_factory=CLIConfigModel)
44
+ hitl: HITLConfigModel = Field(default_factory=HITLConfigModel)
45
+
46
+ @field_validator("tool_paths", mode="before")
47
+ @classmethod
48
+ def _resolve_tool_paths(cls, v: Any) -> list[Path]:
49
+ if v is None:
50
+ return []
51
+ return [Path(p).expanduser() for p in v]
@@ -0,0 +1,9 @@
1
+ """Core orchestration layer for nonoka-cli."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from nonoka_cli.core.context import CLIContext
6
+ from nonoka_cli.core.agent_factory import AgentFactory
7
+ from nonoka_cli.core.orchestrator import Orchestrator
8
+
9
+ __all__ = ["CLIContext", "AgentFactory", "Orchestrator"]
@@ -0,0 +1,78 @@
1
+ """Agent factory — builds nonoka Agent from CLI configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import structlog
8
+
9
+ from nonoka import Agent, AgentBuilder
10
+ from nonoka_cli.config.models import CLIConfig
11
+ from nonoka_cli.utils.errors import AgentBuildError
12
+
13
+ logger = structlog.get_logger("nonoka_cli.core")
14
+
15
+
16
+ class AgentFactory:
17
+ """Builds nonoka Agent instances from CLI configuration.
18
+
19
+ TODO: Integrate MCP tools, local tools, and skills.
20
+ Currently supports model and system_prompt only.
21
+ """
22
+
23
+ def __init__(self, config: CLIConfig):
24
+ self._config = config
25
+ self._agent: Agent | None = None
26
+
27
+ @property
28
+ def config(self) -> CLIConfig:
29
+ """Current configuration."""
30
+ return self._config
31
+
32
+ def build(self) -> Agent:
33
+ """Build (or rebuild) an Agent from the current configuration.
34
+
35
+ Returns:
36
+ A nonoka Agent instance.
37
+
38
+ Raises:
39
+ AgentBuildError: If model is not configured.
40
+ """
41
+ if not self._config.model:
42
+ raise AgentBuildError("No model configured. Set 'model' in config.yaml.")
43
+
44
+ logger.info(
45
+ "building_agent",
46
+ model=self._config.model,
47
+ system_prompt_length=len(self._config.system_prompt),
48
+ )
49
+
50
+ self._agent = (
51
+ AgentBuilder()
52
+ .model(self._config.model)
53
+ .system_prompt(self._config.system_prompt)
54
+ .max_turns(20)
55
+ .build()
56
+ )
57
+ return self._agent
58
+
59
+ def rebuild(self, config_patch: dict[str, Any] | None = None) -> Agent:
60
+ """Rebuild Agent with an optional configuration patch.
61
+
62
+ Args:
63
+ config_patch: Dict of config overrides (e.g. {"model": "gpt-4o"}).
64
+
65
+ Returns:
66
+ The rebuilt Agent.
67
+ """
68
+ if config_patch:
69
+ # Apply patch by creating a new config
70
+ data = self._config.model_dump()
71
+ data.update(config_patch)
72
+ self._config = self._config.__class__.model_validate(data)
73
+
74
+ return self.build()
75
+
76
+ def get_agent(self) -> Agent | None:
77
+ """Return the currently built Agent, if any."""
78
+ return self._agent
@@ -0,0 +1,17 @@
1
+ """CLI runtime context passed to tools via RunContext.deps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from nonoka_cli.config.models import CLIConfig
9
+
10
+
11
+ @dataclass
12
+ class CLIContext:
13
+ """CLI runtime context injected into tools."""
14
+ user: str
15
+ session_id: str
16
+ config: CLIConfig
17
+ working_dir: Path