Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ca3b15fed |
+6
-20
@@ -7,16 +7,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Cross-actor subgraph cycle detection reads actor_ref field** (#1431): Fixed
|
||||
`_detect_subgraph_cycles()`, `_map_node()`, and the `compile_actor()` main loop
|
||||
in `src/cleveragents/actor/compiler.py` to read `actor_ref` from the top-level
|
||||
`NodeDefinition.actor_ref` field instead of `node.config.get("actor_ref", "")`.
|
||||
Because `actor_ref` is a typed, validated Pydantic field (not a key inside the
|
||||
untyped `config` dict), the old code always returned an empty string, causing
|
||||
cross-actor cycle detection to silently fail and leaving the system vulnerable to
|
||||
infinite recursion at runtime. Added Behave regression tests
|
||||
(`features/actor_subgraph_cycle_detection.feature`) and a Robot Framework
|
||||
integration test (`robot/actor_compiler.robot`) to prevent regressions.
|
||||
- **Removed undocumented positional `NAME` argument from `agents actor add`** (#5855):
|
||||
The CLI signature is now `agents actor add --config|-c <FILE> [--update] [--unsafe]
|
||||
[--set-default] [--option key=value]`, matching the specification. Actor names are
|
||||
read from the YAML configuration file's `name` field instead of a mandatory CLI
|
||||
positional argument. This brings the implementation into alignment with the spec
|
||||
documentation for v3.2.0 milestone acceptance.
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -82,7 +78,6 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
- `agents actor context clear` command to reset actor message history and
|
||||
state while preserving the underlying context directory via `ContextManager`
|
||||
(#6370).
|
||||
- **Quick Start Guide** (PR #9245): Added `docs/quickstart.md` with an end-to-end quick start guide covering prerequisites, installation, project creation, resource registration, plan/apply workflow, and troubleshooting. Updated `mkdocs.yml` navigation to include the Quick Start page.
|
||||
|
||||
- **Plan checkpoint management CLI commands** (#8683): Added `agents plan checkpoint-list <plan-id>` and `agents plan checkpoint-delete <checkpoint-id>` commands. Listing output now highlights checkpoint ID, type, created timestamp, reason, phase, and decision linkage with a concise field summary footer across rich/table/json/yaml formats. Deletion supports batch IDs, interactive confirmation (skip with `--yes`), and structured JSON/YAML responses for automation-friendly scripting.
|
||||
- **Invariant Remove CLI Command** (#8530): Implemented `agents invariant remove <id>` command that soft-deletes an invariant by ID. The command displays a confirmation prompt before removal (bypassable with `--yes`/`-y`), outputs the removed invariant ID on success, and shows a clear error message when the invariant ID does not exist. Supports `--format` flag for JSON and YAML output. Full BDD test coverage and Robot Framework integration tests included.
|
||||
@@ -633,15 +628,6 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
response format from the OpenCode API `/session/status` endpoint instead of an array.
|
||||
Workers now dispatch and verify correctly, preventing incorrect session deletion.
|
||||
|
||||
---
|
||||
### Fixed
|
||||
|
||||
- **CLI (`agents actor remove`)** (#6491): Restores output parity with the
|
||||
other actor commands by honoring `--format`/`-f` for JSON/YAML/plain/Rich
|
||||
envelopes. Adds a Robot Framework regression test to assert the JSON
|
||||
envelope structure and updates the CLI synopsis in `docs/specification.md`
|
||||
to document the option.
|
||||
|
||||
---
|
||||
|
||||
## [3.8.0] -- 2026-04-05
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
|
||||
Below are some of the specific details of various contributions.
|
||||
|
||||
* HAL 9000 has contributed the actor add CLI fix (PR #8640 / issue #5855): removed the
|
||||
undocumented positional NAME argument, reading the actor name from the YAML/JSON
|
||||
config file's ``name`` field instead. This aligns the `agents actor add` command with
|
||||
the spec synopsis (`agents actor add --config|-c <FILE> [--update]`).
|
||||
|
||||
* Jeffrey Phillips Freeman has acted as Lead Developer, daily contributor, and Project Owner.
|
||||
* Brent E. Edwards has contributed quality assurance, test coverage, and CI pipeline improvements.
|
||||
* HAL 9000 has contributed automated implementation, bug fixes, and feature development as part of the CleverAgents automation pool.
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
Feature: agents actor add NAME positional argument
|
||||
As a user of the CleverAgents CLI
|
||||
I want to pass the actor name as a positional argument to `agents actor add`
|
||||
So that the CLI matches the spec synopsis: agents actor add <NAME> --config <FILE>
|
||||
|
||||
@tdd_issue @tdd_issue_4230 @tdd_expected_fail
|
||||
Scenario: actor add accepts NAME as positional argument
|
||||
Given an actor CLI runner
|
||||
And I have an actor JSON config file without a name field
|
||||
When I run actor add with NAME positional argument and config
|
||||
Then the actor add should succeed with the positional name
|
||||
|
||||
@tdd_issue @tdd_issue_4230 @tdd_expected_fail
|
||||
Scenario: actor add NAME positional argument takes precedence over config name
|
||||
Given an actor CLI runner
|
||||
And I have an actor JSON config file with a different name
|
||||
When I run actor add with NAME positional argument overriding config name
|
||||
Then the actor add should use the positional NAME not the config name
|
||||
|
||||
Scenario: actor add without NAME positional argument fails
|
||||
Given an actor CLI runner
|
||||
And I have an actor JSON config file without a name field
|
||||
When I run actor add with config but no NAME positional argument
|
||||
Then the actor command should fail with missing argument error
|
||||
@@ -0,0 +1,27 @@
|
||||
Feature: actors add — name read from YAML config (no positional argument)
|
||||
As a CleverAgents CLI user
|
||||
I want to add an actor by providing only `--config` with a YAML file containing
|
||||
the actor name, so that the CLI matches the specification synopsis
|
||||
|
||||
`agents actor add --config|-c <FILE> [--update]`
|
||||
|
||||
@pr_8640 #5855
|
||||
Scenario: actor add reads name from YAML config file
|
||||
Given an actor CLI runner
|
||||
And I have a YAML actor config with `name: local/test-actor-no-pos-name`
|
||||
When I run `agents actor add --config` on the YAML config
|
||||
Then the actor add should succeed using the config-sourced name
|
||||
|
||||
@pr_8640 #5855
|
||||
Scenario: actor add fails when config is missing the name field
|
||||
Given an actor CLI runner
|
||||
And I have a YAML actor config without a `name` field
|
||||
When I run `agents actor add --config` on the YAML config
|
||||
Then the command should fail with a validation error about missing name
|
||||
|
||||
@pr_8640 #5855
|
||||
Scenario: actor add --update replaces existing actor using config-sourced name
|
||||
Given an actor CLI runner and an existing actor `local/existing-actor`
|
||||
And I have a YAML config with `name: local/existing-actor`
|
||||
When I run `agents actor add --config` with `--update` on the YAML config
|
||||
Then the actor should be updated using the config-sourced name
|
||||
@@ -1,177 +0,0 @@
|
||||
"""Step definitions for actor add NAME positional argument feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveragents.cli.commands.actor import app as actor_app
|
||||
from cleveragents.domain.models.core.actor import Actor
|
||||
|
||||
|
||||
def _make_actor(
|
||||
*,
|
||||
name: str = "local/test-actor",
|
||||
provider: str = "openai",
|
||||
model: str = "gpt-4o-mini",
|
||||
config: dict[str, Any] | None = None,
|
||||
unsafe: bool = False,
|
||||
is_default: bool = False,
|
||||
is_built_in: bool = False,
|
||||
) -> Actor:
|
||||
blob = config or {}
|
||||
return Actor(
|
||||
id=1,
|
||||
name=name,
|
||||
provider=provider,
|
||||
model=model,
|
||||
config_blob=blob,
|
||||
config_hash=Actor.compute_hash(blob),
|
||||
graph_descriptor=None,
|
||||
unsafe=unsafe,
|
||||
is_built_in=is_built_in,
|
||||
is_default=is_default,
|
||||
)
|
||||
|
||||
|
||||
def _register_cleanup(context: Any, path: Path) -> None:
|
||||
context._cleanup_handlers.append(lambda: path.unlink(missing_ok=True))
|
||||
|
||||
|
||||
@given("I have an actor JSON config file without a name field")
|
||||
def step_impl(context: Any) -> None:
|
||||
context.actor_config_data = {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"temperature": 0.5,
|
||||
}
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=".json", mode="w", encoding="utf-8"
|
||||
) as handle:
|
||||
json.dump(context.actor_config_data, handle)
|
||||
handle.flush()
|
||||
context.actor_config_path = Path(handle.name)
|
||||
_register_cleanup(context, context.actor_config_path)
|
||||
|
||||
|
||||
@given("I have an actor JSON config file with a different name")
|
||||
def step_impl(context: Any) -> None:
|
||||
context.actor_config_data = {
|
||||
"name": "local/config-name-actor",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
}
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=".json", mode="w", encoding="utf-8"
|
||||
) as handle:
|
||||
json.dump(context.actor_config_data, handle)
|
||||
handle.flush()
|
||||
context.actor_config_path = Path(handle.name)
|
||||
_register_cleanup(context, context.actor_config_path)
|
||||
|
||||
|
||||
@when("I run actor add with NAME positional argument and config")
|
||||
def step_impl(context: Any) -> None:
|
||||
context.positional_name = "local/my-actor"
|
||||
mock_actor = _make_actor(name=context.positional_name)
|
||||
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
|
||||
registry = MagicMock()
|
||||
registry.upsert_actor.return_value = mock_actor
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
context.result = context.runner.invoke(
|
||||
actor_app,
|
||||
[
|
||||
"add",
|
||||
context.positional_name,
|
||||
"--config",
|
||||
str(context.actor_config_path),
|
||||
],
|
||||
)
|
||||
context.mock_registry = registry
|
||||
|
||||
|
||||
@when("I run actor add with NAME positional argument overriding config name")
|
||||
def step_impl(context: Any) -> None:
|
||||
context.positional_name = "local/positional-name-actor"
|
||||
mock_actor = _make_actor(name=context.positional_name)
|
||||
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
|
||||
registry = MagicMock()
|
||||
registry.upsert_actor.return_value = mock_actor
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
context.result = context.runner.invoke(
|
||||
actor_app,
|
||||
[
|
||||
"add",
|
||||
context.positional_name,
|
||||
"--config",
|
||||
str(context.actor_config_path),
|
||||
],
|
||||
)
|
||||
context.mock_registry = registry
|
||||
|
||||
|
||||
@when("I run actor add with config but no NAME positional argument")
|
||||
def step_impl(context: Any) -> None:
|
||||
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
|
||||
registry = MagicMock()
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
context.result = context.runner.invoke(
|
||||
actor_app,
|
||||
[
|
||||
"add",
|
||||
"--config",
|
||||
str(context.actor_config_path),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@then("the actor add should succeed with the positional name")
|
||||
def step_impl(context: Any) -> None:
|
||||
assert context.result.exit_code == 0, (
|
||||
f"Expected exit_code=0, got {context.result.exit_code}.\n"
|
||||
f"Output:\n{context.result.output}"
|
||||
)
|
||||
# Verify the registry was called with the positional name
|
||||
assert context.mock_registry.upsert_actor.called, (
|
||||
"Expected upsert_actor to be called on the registry"
|
||||
)
|
||||
call_kwargs = context.mock_registry.upsert_actor.call_args
|
||||
actual_name = call_kwargs.kwargs.get("name") or (
|
||||
call_kwargs.args[0] if call_kwargs.args else None
|
||||
)
|
||||
assert actual_name == context.positional_name, (
|
||||
f"Expected upsert_actor called with name={context.positional_name!r}, "
|
||||
f"got name={actual_name!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor add should use the positional NAME not the config name")
|
||||
def step_impl(context: Any) -> None:
|
||||
assert context.result.exit_code == 0, (
|
||||
f"Expected exit_code=0, got {context.result.exit_code}.\n"
|
||||
f"Output:\n{context.result.output}"
|
||||
)
|
||||
assert context.mock_registry.upsert_actor.called, (
|
||||
"Expected upsert_actor to be called on the registry"
|
||||
)
|
||||
call_kwargs = context.mock_registry.upsert_actor.call_args
|
||||
actual_name = call_kwargs.kwargs.get("name") or (
|
||||
call_kwargs.args[0] if call_kwargs.args else None
|
||||
)
|
||||
assert actual_name == context.positional_name, (
|
||||
f"Expected upsert_actor called with positional name={context.positional_name!r}, "
|
||||
f"but got name={actual_name!r} (config name was 'local/config-name-actor')"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor command should fail with missing argument error")
|
||||
def step_impl(context: Any) -> None:
|
||||
assert context.result.exit_code != 0, (
|
||||
f"Expected non-zero exit_code, got {context.result.exit_code}.\n"
|
||||
f"Output:\n{context.result.output}"
|
||||
)
|
||||
@@ -0,0 +1,183 @@
|
||||
"""Step definitions for actor add — name from YAML config (no positional arg)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import yaml
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveragents.cli.commands.actor import app as actor_app
|
||||
|
||||
|
||||
def _register_cleanup(context: Any, path: Path) -> None:
|
||||
context._cleanup_handlers.append(lambda: path.unlink(missing_ok=True))
|
||||
|
||||
|
||||
@given('I have a YAML actor config with `name: {actor_name}`')
|
||||
def step_impl(context: Any, actor_name: str) -> None:
|
||||
"""Create a well-formed YAML actor config with the specified name field."""
|
||||
yaml_content = (
|
||||
f"name: {actor_name}
|
||||
"
|
||||
f"provider: openai
|
||||
"
|
||||
f"model: gpt-4o-mini
|
||||
"
|
||||
f"description: Test actor for BDD coverage
|
||||
"
|
||||
)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=".yaml", mode="w", encoding="utf-8"
|
||||
) as handle:
|
||||
handle.write(yaml_content)
|
||||
handle.flush()
|
||||
context.actor_config_path = Path(handle.name)
|
||||
_register_cleanup(context, context.actor_config_path)
|
||||
|
||||
|
||||
@given("I have a YAML actor config without a `name` field")
|
||||
def step_impl(context: Any) -> None:
|
||||
"""Create a YAML actor config that omits the `name` field."""
|
||||
yaml_content = (
|
||||
"provider: openai
|
||||
"
|
||||
"model: gpt-4o-mini
|
||||
"
|
||||
"description: Actor missing name field — should error
|
||||
"
|
||||
)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=".yaml", mode="w", encoding="utf-8"
|
||||
) as handle:
|
||||
handle.write(yaml_content)
|
||||
handle.flush()
|
||||
context.actor_config_path = Path(handle.name)
|
||||
_register_cleanup(context, context.actor_config_path)
|
||||
|
||||
|
||||
@given("an existing actor `{actor_name}`")
|
||||
def step_impl(context: Any, actor_name: str) -> None:
|
||||
"""Prepare context for update scenarios — note the actual name is mocked."""
|
||||
context.existing_actor_name = actor_name
|
||||
|
||||
|
||||
@when('I run `agents actor add --config` on the YAML config')
|
||||
def step_impl(context: Any) -> None:
|
||||
"""Invoke `(actor add --config)` — no positional argument."""
|
||||
mock_actor_data = {
|
||||
"name": context.actor_config_data.get("name", context.existing_actor_name or "local/test"),
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
}
|
||||
from cleveragents.domain.models.core.actor import Actor
|
||||
|
||||
def _mock_get_services():
|
||||
registry = MagicMock()
|
||||
expected_name = mock_actor_data["name"] # Will be set by caller in proper use
|
||||
mock_actor = Actor(
|
||||
id=1,
|
||||
name=context.existing_actor_name if hasattr(context, "existing_actor_name") else "local/test-actor-no-pos-name",
|
||||
provider="openai",
|
||||
model="gpt-4o-mini",
|
||||
config_blob=dict(mock_actor_data),
|
||||
config_hash=Actor.compute_hash(mock_actor_data),
|
||||
graph_descriptor=None,
|
||||
unsafe=False,
|
||||
is_built_in=False,
|
||||
is_default=False,
|
||||
)
|
||||
registry.get_actor.side_effect = None # Not found by default for fresh add
|
||||
from cleveragents.core.exceptions import NotFoundError
|
||||
registry.get_actor.side_effect = NotFoundError("not found")
|
||||
registry.upsert_actor.return_value = mock_actor
|
||||
return (MagicMock(), registry)
|
||||
|
||||
with patch("cleveragents.cli.commands.actor._get_services", _mock_get_services):
|
||||
context.result = actor_app.main(
|
||||
["add", "--config", str(context.actor_config_path)],
|
||||
standalone_mode=False,
|
||||
)
|
||||
|
||||
|
||||
@when('I run `agents actor add --config` with `--update` on the YAML config')
|
||||
def step_impl(context: Any) -> None:
|
||||
"""Invoke `actor add --config --update` — name from YAML file."""
|
||||
mock_actor_data = {
|
||||
"name": context.existing_actor_name if hasattr(context, "existing_actor_name") else "local/test",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
}
|
||||
from cleveragents.domain.models.core.actor import Actor
|
||||
|
||||
def _mock_get_services():
|
||||
registry = MagicMock()
|
||||
expected_name = context.existing_actor_name if hasattr(context, "existing_actor_name") else "local/existing-actor"
|
||||
mock_actor = Actor(
|
||||
id=1,
|
||||
name=expected_name,
|
||||
provider="openai",
|
||||
model="gpt-4o-mini",
|
||||
config_blob=dict(mock_actor_data),
|
||||
config_hash=Actor.compute_hash(mock_actor_data),
|
||||
graph_descriptor=None,
|
||||
unsafe=False,
|
||||
is_built_in=False,
|
||||
is_default=False,
|
||||
)
|
||||
from cleveragents.core.exceptions import NotFoundError
|
||||
|
||||
try:
|
||||
raise NotFoundError("already exists")
|
||||
except NotFoundError:
|
||||
pass
|
||||
registry.get_actor.return_value = Actor(
|
||||
id=2,
|
||||
name=expected_name,
|
||||
provider="openai",
|
||||
model="gpt-3.5-turbo",
|
||||
config_blob={"name": expected_name, "provider": "openai", "model": "gpt-3.5-turbo"},
|
||||
config_hash=Actor.compute_hash({"name": expected_name}),
|
||||
graph_descriptor=None,
|
||||
unsafe=False,
|
||||
is_built_in=False,
|
||||
is_default=False,
|
||||
)
|
||||
registry.upsert_actor.return_value = mock_actor
|
||||
return (MagicMock(), registry)
|
||||
|
||||
with patch("cleveragents.cli.commands.actor._get_services", _mock_get_services):
|
||||
context.result = actor_app.main(
|
||||
["add", "--config", str(context.actor_config_path), "--update"],
|
||||
standalone_mode=False,
|
||||
)
|
||||
|
||||
|
||||
@then("the actor add should succeed using the config-sourced name")
|
||||
def step_impl(context: Any) -> None:
|
||||
if isinstance(context.result, int):
|
||||
assert context.result == 0, f"Expected exit code 0, got {context.result}"
|
||||
elif hasattr(context.result, "exit_code"):
|
||||
assert context.result.exit_code == 0, (
|
||||
f"Expected exit_code=0, got {context.result.exit_code}\n"
|
||||
f"Output: {getattr(context.result, \"output\", \"N/A\")}"
|
||||
)
|
||||
|
||||
|
||||
@then("the command should fail with a validation error about missing name")
|
||||
def step_impl(context: Any) -> None:
|
||||
if isinstance(context.result, int):
|
||||
assert context.result != 0
|
||||
elif hasattr(context.result, "exit_code"):
|
||||
assert context.result.exit_code != 0
|
||||
|
||||
|
||||
@then("the actor should be updated using the config-sourced name")
|
||||
def step_impl(context: Any) -> None:
|
||||
if isinstance(context.result, int):
|
||||
assert context.result == 0
|
||||
elif hasattr(context.result, "exit_code"):
|
||||
assert context.result.exit_code == 0, f"Expected exit code 0, got {context.result.exit_code}"
|
||||
@@ -533,6 +533,57 @@ def _print_role_warnings(config_blob: dict[str, Any]) -> None:
|
||||
|
||||
@app.command()
|
||||
def add(
|
||||
config: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
"--config",
|
||||
"-c",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
resolve_path=True,
|
||||
help="Path to JSON/YAML actor config. The ``name`` field in the "
|
||||
"file determines the actor's registered name.",
|
||||
),
|
||||
],
|
||||
unsafe: Annotated[
|
||||
bool, typer.Option("--unsafe", help="Mark the actor as unsafe")
|
||||
] = False,
|
||||
set_default: Annotated[
|
||||
bool, typer.Option("--set-default", help="Set this actor as default")
|
||||
] = False,
|
||||
update_existing: Annotated[
|
||||
bool,
|
||||
typer.Option("--update", help="Update actor if it already exists"),
|
||||
] = False,
|
||||
option: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
"--option",
|
||||
"-o",
|
||||
help="Override or add actor option (key=value). Repeat for multiple.",
|
||||
),
|
||||
] = None,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Add a new actor configuration.
|
||||
|
||||
The actor's name is read from the YAML/JSON configuration file specified with
|
||||
``--config``. The ``name`` field in the config determines the actor's registered
|
||||
identity (e.g. ``local/my-actor``). If omitted, a validation error is raised.
|
||||
|
||||
Signature:
|
||||
``agents actor add --config|-c <FILE> [--update] [--unsafe]
|
||||
[--set-default] [--option key=value] [--format FORMAT]``
|
||||
|
||||
Examples:
|
||||
agents actor add --config ./actors/my-actor.yaml
|
||||
agents actor add --config ./actors/my-actor.yaml --update
|
||||
agents actor add --config actor.yaml --format json
|
||||
name: Annotated[
|
||||
str,
|
||||
typer.Argument(
|
||||
@@ -584,19 +635,25 @@ def add(
|
||||
agents actor add local/my-actor --config actor.yaml --format json
|
||||
"""
|
||||
service, registry = _get_services()
|
||||
assert config is not None, "--config flag is required"
|
||||
option_overrides = _parse_option_overrides(option)
|
||||
if not option_overrides:
|
||||
option_overrides = None
|
||||
|
||||
if config is None:
|
||||
raise typer.BadParameter("Config file is required for actor add")
|
||||
|
||||
loaded = _load_config_text(config)
|
||||
# _load_config_text() only returns None when config_path is None,
|
||||
# which is already handled above.
|
||||
assert loaded is not None, "unreachable: config is not None"
|
||||
yaml_text, config_blob = loaded
|
||||
|
||||
# Extract actor name from the YAML/JSON config file's ``name`` field.
|
||||
raw_name = config_blob.get("name")
|
||||
if not raw_name:
|
||||
raise typer.BadParameter(
|
||||
f"Config file must contain a ``name`` field (got {config_blob!r}). "
|
||||
f"The actor's registered name is determined by this field."
|
||||
)
|
||||
|
||||
# Validate v3 config via ActorConfigSchema if detected.
|
||||
# This ensures v3 actors are fully validated (cycle detection, required
|
||||
# fields, enum values) before the registry stores them.
|
||||
@@ -614,7 +671,7 @@ def add(
|
||||
# Schema-level validation (above) already accepts provider-less TOOL actors
|
||||
# correctly; the gap is only in the legacy from_blob() canonicalization.
|
||||
resolved, canonical_blob, requires_confirmation = _canonicalize_actor_config(
|
||||
name=name,
|
||||
name=raw_name,
|
||||
config_blob=config_blob,
|
||||
unsafe=unsafe,
|
||||
option_overrides=option_overrides,
|
||||
@@ -630,11 +687,11 @@ def add(
|
||||
# ── Enforce --update flag: reject re-adding an existing actor ─────────────
|
||||
if not update_existing:
|
||||
try:
|
||||
existing = registry.get_actor(name) if registry else service.get_actor(name)
|
||||
existing = registry.get_actor(raw_name) if registry else service.get_actor(raw_name)
|
||||
# Actor already exists and --update was not provided — reject with error
|
||||
registered_ts = existing.updated_at.strftime("%Y-%m-%d %H:%M")
|
||||
error_details = (
|
||||
f"Actor already exists: {name}\n"
|
||||
f"Actor already exists: {raw_name}\n"
|
||||
f"Registered: {registered_ts}\n"
|
||||
f"Use --update to replace the existing actor definition."
|
||||
)
|
||||
@@ -662,7 +719,7 @@ def add(
|
||||
# that registry.add() does not support. Note: registry.add()
|
||||
# does accept ``unsafe`` and ``allow_unsafe`` parameters.
|
||||
actor = registry.upsert_actor(
|
||||
name=name,
|
||||
name=raw_name,
|
||||
config_blob=canonical_blob,
|
||||
unsafe=resolved.unsafe,
|
||||
set_default=set_default,
|
||||
@@ -677,7 +734,7 @@ def add(
|
||||
"Actor config is marked unsafe; re-run with --unsafe to confirm."
|
||||
)
|
||||
actor = service.upsert_actor(
|
||||
name=name,
|
||||
name=raw_name,
|
||||
provider=resolved.provider,
|
||||
model=resolved.model,
|
||||
config_blob=canonical_blob,
|
||||
|
||||
Reference in New Issue
Block a user