Skip to content

feat(config): add vision_agents.config with load_settings#566

Draft
aliev wants to merge 1 commit into
mainfrom
experiments/config
Draft

feat(config): add vision_agents.config with load_settings#566
aliev wants to merge 1 commit into
mainfrom
experiments/config

Conversation

@aliev

@aliev aliev commented May 20, 2026

Copy link
Copy Markdown
Member

Why

vision-agents agent reads the project's pyproject.toml to find [tool.vision-agents.agent].entrypoint. The TOML parsing, walk-up to the nearest pyproject.toml, and entrypoint validation all currently live inside cli/agent/config.py — fine while there's only one consumer, but they're not CLI-domain concerns. They're project-level config primitives. If a second consumer (a future subcommand, a plugin, or user code inside agent.py) wants to read [tool.vision-agents.<something>], today they'd have to either duplicate the walk-up logic or import from cli.agent, which is the wrong direction.

This PR promotes those primitives into a top-level vision_agents.config module, peer to core, cli, plugins. The CLI keeps its CLI-specific concerns (wrapping neutral exceptions into CliError, building ResolvedEntrypoint) but no longer owns TOML reading or entrypoint format validation. Anyone who needs project config — including a user importing it from their own agent.py — can call load_settings() and get a typed Settings instance.

from vision_agents.config import load_settings

settings = load_settings()              # walks up from cwd
settings.project_root                   # Path
settings.pyproject_path                 # Path
settings.agent.entrypoint               # "agent:runner" (or None if section missing)
settings.raw["custom-section"]          # escape hatch for unmodeled subsections

Errors are also relocated to the right layer: find_pyproject raises FileNotFoundError (stdlib, neutral), load_settings raises ValueError for schema problems. The CLI catches both at the boundary and re-wraps to CliError so the red Error: … UX is unchanged.

What's exported

  • load_settings(cwd: Path | None = None) -> Settings
  • Settings — frozen dataclass with project_root, pyproject_path, agent: AgentSettings | None, raw: dict
  • AgentSettings — frozen dataclass with module, attribute, and an entrypoint property
  • find_pyproject(start: Path) -> Path — the walk-up helper
  • parse_entrypoint(spec: object) -> tuple[str, str] — gunicorn-style module:attribute validator
  • TOOL_TABLE = "tool.vision-agents" and PYPROJECT_FILENAME = "pyproject.toml" constants

Why this is an experiment branch

The new module is intentionally small (one agent section, plus a raw escape hatch) — there's no deploy, no dev, no lint. The shape is locked enough to be useful but not over-fitted. Marking this experiments/ lets us merge or iterate based on how the API feels in real plugin/user code before committing to the public surface.

Promotes project-level config loading out of `cli/agent/config.py` into a
top-level `vision_agents.config` module (peer to `core`, `cli`, `plugins`).

Exposes:
- `load_settings(cwd=None) -> Settings` — walks up to the nearest
  `pyproject.toml` and parses `[tool.vision-agents.*]` into a `Settings`
  dataclass.
- `Settings` (with `agent: AgentSettings | None` + a `raw` escape hatch
  for plugin or user code reading unmodeled subsections).
- `find_pyproject(start)` and `parse_entrypoint(spec)` — the lower-level
  helpers, now reusable outside the CLI.

`cli/agent/config.py` shrinks to ~50 lines: it only wraps the neutral
`FileNotFoundError` / `ValueError` from `load_settings` into CLI-domain
`CliError`, then composes a `ResolvedEntrypoint`. All TOML parsing,
walk-up logic, and entrypoint validation now live in one place that the
CLI, future subcommands, plugins, and even user `agent.py` files can
import.
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces a new centralized configuration module that discovers and parses pyproject.toml to load Vision Agents settings. Defines AgentSettings and Settings dataclasses to represent agent entrypoint configuration and project paths. Provides utility functions to find pyproject.toml by walking upward, parse module:attribute entrypoint strings with validation, and load/resolve configuration into a Settings instance. Refactors the CLI config module to import these shared utilities, removes duplicate local parsing code, and updates resolve_entrypoint to delegate to the new shared infrastructure.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1e735cfb-87c3-46dd-a14d-ade8110c6452

📥 Commits

Reviewing files that changed from the base of the PR and between e192bd0 and 0b670c8.

📒 Files selected for processing (2)
  • agents-core/vision_agents/cli/agent/config.py
  • agents-core/vision_agents/config.py

Comment on lines +76 to +80
module, _, attribute = spec.partition(":")
if not module or not attribute:
raise ValueError(
f"entrypoint {spec!r} is malformed; expected 'module:attribute'"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Trim entrypoint segments before emptiness checks.

"pkg :runner" and "pkg: runner" currently pass validation but produce downstream import errors. Normalize whitespace in parser so invalid forms fail early with config errors.

Proposed fix
-    module, _, attribute = spec.partition(":")
+    module, _, attribute = spec.partition(":")
+    module = module.strip()
+    attribute = attribute.strip()
     if not module or not attribute:
         raise ValueError(
             f"entrypoint {spec!r} is malformed; expected 'module:attribute'"
         )

Comment on lines +112 to +116
tool = data.get("tool")
raw = tool.get("vision-agents") if isinstance(tool, dict) else None
if not isinstance(raw, dict):
raw = {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Raise ValueError when [tool.vision-agents] exists but is not a table.

Current logic silently downgrades invalid schema to {}. That violates the loader’s malformed-schema contract and can hide user config mistakes.

Proposed fix
-    tool = data.get("tool")
-    raw = tool.get("vision-agents") if isinstance(tool, dict) else None
-    if not isinstance(raw, dict):
-        raw = {}
+    tool = data.get("tool")
+    if tool is not None and not isinstance(tool, dict):
+        raise ValueError("[tool] must be a table")
+
+    raw_section = tool.get("vision-agents") if isinstance(tool, dict) else None
+    if raw_section is not None and not isinstance(raw_section, dict):
+        raise ValueError(f"[{TOOL_TABLE}] must be a table")
+    raw = raw_section if isinstance(raw_section, dict) else {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tool = data.get("tool")
raw = tool.get("vision-agents") if isinstance(tool, dict) else None
if not isinstance(raw, dict):
raw = {}
tool = data.get("tool")
if tool is not None and not isinstance(tool, dict):
raise ValueError("[tool] must be a table")
raw_section = tool.get("vision-agents") if isinstance(tool, dict) else None
if raw_section is not None and not isinstance(raw_section, dict):
raise ValueError(f"[{TOOL_TABLE}] must be a table")
raw = raw_section if isinstance(raw_section, dict) else {}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant