Compare commits

..

8 Commits

Author SHA1 Message Date
freemo 450e118b1a fix(resources): remove unsupported executable resource type
The executable resource type was not defined in the specification
(docs/specification.md lines 10567-10575). The supported resource types
are: git-checkout, git, fs-mount, fs-directory, fs-file,
container-instance, and devcontainer-instance.

Additionally, the agents resource list output included a Location column
that is not part of the spec-defined columns (Name, ID, Type, Phys/Virt,
Children, Projects per line 11051).

Changes:
- Remove executable type registration from _resource_registry_lsp.py
- Remove executable from BUILTIN_TYPE_NAMES in _resource_type_validation.py
- Update resource list table columns to match spec (Name, ID, Type,
  Phys/Virt, Children, Projects) removing Location and Description
- Remove all executable-related test scenarios from
  features/resource_type_lsp.feature
- Update robot/helper_resource_type_lsp.py to reflect 3 LSP types
  (not 4) and add assertions that executable is absent

ISSUES CLOSED: #3077
2026-05-07 10:53:53 +00:00
HAL9000 f2d1f4efe7 docs: fix quickstart plan/apply command signatures
CI / push-validation (push) Successful in 38s
CI / helm (push) Successful in 47s
CI / build (push) Successful in 1m2s
CI / lint (push) Successful in 1m14s
CI / quality (push) Successful in 1m23s
CI / typecheck (push) Successful in 1m47s
CI / security (push) Successful in 1m59s
CI / integration_tests (push) Successful in 4m2s
CI / e2e_tests (push) Successful in 4m35s
CI / unit_tests (push) Failing after 6m19s
CI / coverage (push) Has been skipped
CI / docker (push) Has been skipped
CI / status-check (push) Failing after 3s
CI / benchmark-regression (push) Has been skipped
CI / benchmark-publish (push) Successful in 1h17m32s
CI / benchmark-publish (pull_request) Has been skipped
CI / push-validation (pull_request) Successful in 43s
CI / helm (pull_request) Successful in 47s
CI / lint (pull_request) Successful in 1m21s
CI / quality (pull_request) Successful in 1m32s
CI / build (pull_request) Successful in 1m18s
CI / security (pull_request) Successful in 1m39s
CI / benchmark-regression (pull_request) Failing after 1m23s
CI / typecheck (pull_request) Successful in 2m14s
CI / integration_tests (pull_request) Successful in 4m16s
CI / unit_tests (pull_request) Failing after 4m59s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 4m52s
CI / status-check (pull_request) Failing after 3s
Update docs/quickstart.md to use correct CLI command signatures:
- Replace 'cleveragents plan --project' with 'cleveragents plan use <action> --project'
- Replace 'cleveragents apply --project' with 'cleveragents plan list' + 'cleveragents plan apply <plan-id>'

Addresses reviewer non-blocking suggestion to verify CLI commands match
actual signatures before merging.
2026-05-06 04:29:40 +00:00
HAL9000 54fef4768c docs: add quick start guide (PR #9245)
Add docs/quickstart.md with end-to-end quick start guide covering
prerequisites, installation, project creation, resource registration,
plan/apply workflow, and troubleshooting.

Update mkdocs.yml navigation to include Quick Start page and rename
ADR-028 title to Skill/Tool Abstraction Definition.

Update CHANGELOG.md with quick start guide entry.

Closes #9245
2026-05-06 04:29:40 +00:00
HAL9000 0461f8e51f fix(actor): remove type: ignore from subgraph cycle detection steps
CI / benchmark-publish (push) Has started running
CI / benchmark-regression (push) Has been skipped
CI / push-validation (push) Successful in 47s
CI / helm (push) Successful in 58s
CI / build (push) Successful in 59s
CI / lint (push) Successful in 1m28s
CI / typecheck (push) Successful in 1m37s
CI / security (push) Successful in 1m37s
CI / quality (push) Successful in 1m44s
CI / e2e_tests (push) Failing after 4m41s
CI / integration_tests (push) Failing after 7m36s
CI / unit_tests (push) Failing after 9m48s
CI / coverage (push) Has been skipped
CI / docker (push) Has been skipped
CI / status-check (push) Failing after 4s
Remove # type: ignore[import-untyped] suppression comments from
features/steps/actor_subgraph_cycle_detection_steps.py per
CONTRIBUTING.md zero-tolerance policy on inline type suppressions.
Pyright does not check the features/ directory so these suppressions
were unnecessary and violated project policy.
2026-05-06 04:08:25 +00:00
HAL9000 5db663cb63 fix(actor): read actor_ref from NodeDefinition field in _detect_subgraph_cycles
Fix cross-actor subgraph cycle detection in the actor compiler by reading
actor_ref from the top-level NodeDefinition field instead of the config dict.

The _detect_subgraph_cycles(), _map_node(), and compile_actor() functions
were all reading node.config.get("actor_ref", "") instead of node.actor_ref.
Because actor_ref is a typed, validated Pydantic field on NodeDefinition (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.

Changes:
- Fix all three call sites in src/cleveragents/actor/compiler.py
- Add Behave regression tests (features/actor_subgraph_cycle_detection.feature)
  with @tdd_issue and @tdd_issue_1431 tags on all scenarios
- Add Robot Framework integration test (robot/actor_compiler.robot)
- Fix existing test helpers to use actor_ref as top-level field
- Add CHANGELOG entry

ISSUES CLOSED: #1431
2026-05-06 04:08:25 +00:00
HAL9000 ad31e75af6 fix(cli): address reviewer feedback on actor remove --format option
CI / benchmark-regression (push) Has been skipped
CI / push-validation (push) Successful in 38s
CI / helm (push) Successful in 56s
CI / build (push) Successful in 1m9s
CI / lint (push) Successful in 1m27s
CI / quality (push) Successful in 1m26s
CI / typecheck (push) Successful in 1m36s
CI / security (push) Successful in 1m35s
CI / integration_tests (push) Successful in 4m20s
CI / e2e_tests (push) Successful in 5m37s
CI / unit_tests (push) Failing after 6m57s
CI / coverage (push) Has been skipped
CI / docker (push) Has been skipped
CI / status-check (push) Failing after 3s
CI / benchmark-publish (push) Successful in 1h17m19s
CI / benchmark-publish (pull_request) Has been skipped
CI / push-validation (pull_request) Successful in 35s
CI / helm (pull_request) Successful in 42s
CI / lint (pull_request) Successful in 1m7s
CI / build (pull_request) Successful in 1m1s
CI / quality (pull_request) Successful in 1m14s
CI / typecheck (pull_request) Successful in 1m28s
CI / benchmark-regression (pull_request) Failing after 56s
CI / security (pull_request) Successful in 1m50s
CI / integration_tests (pull_request) Successful in 4m24s
CI / e2e_tests (pull_request) Successful in 5m16s
CI / unit_tests (pull_request) Failing after 7m54s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 4s
- Validate --format argument before any side effects; raise typer.BadParameter
  with a clear message for unsupported format values (fail-fast principle)
- Pass normalised fmt_value (lowercased) to format_output instead of raw fmt
  to ensure consistent behaviour regardless of input casing
- Rewrite robot/helper_actor_remove_cli.py to exercise the real CLI end-to-end
  via subprocess (no mocking); seeds a test actor via agents actor add, then
  removes it with --format json and validates the JSON envelope

ISSUES CLOSED: #6491
2026-05-06 01:07:15 +00:00
HAL9000 8384f53e28 test(cli): cover actor remove format regression (#6491)
Add Robot regression coverage for `actor remove --format json`, document the flag in the CLI synopsis, and record the fix in the changelog.\n\nISSUES CLOSED: #6491
2026-05-06 01:07:15 +00:00
HAL9000 defa04d56d fix(cli): add --format option to actor remove command (#6491)
ISSUES CLOSED: #6491
2026-05-06 01:07:15 +00:00
26 changed files with 739 additions and 273 deletions
+23 -6
View File
@@ -5,6 +5,19 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
### 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.
### Changed
- **`agents session list` now displays full 26-character session ULIDs** (#10970): The Rich table
@@ -69,6 +82,7 @@ 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.
@@ -343,12 +357,6 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
had `@tdd_expected_fail` removed and now run as permanent regression guards.
Net result: 629 features active in CI (up from ~545), zero `@skip` tags remain.
- **Actor Context List Regex Filter** (#6500): `agents actor context list` now
accepts an optional `[REGEX]` positional argument to filter listed named
contexts by a regular expression pattern. Invalid patterns are reported with
a clear error message. The deprecated `agents context list` alias receives
the same filtering capability.
- **Git Worktree Sandbox Apply** (#4454): The `plan apply` command now merges
LLM-generated changes via `git merge` from an isolated worktree branch
instead of flat `shutil.copy2`. Displays spec-aligned Apply Summary
@@ -625,6 +633,15 @@ 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
-1
View File
@@ -31,4 +31,3 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed comprehensive milestone documentation for v3.6.0 (Advanced Concepts & Deferred Features) and v3.7.0 (TUI Implementation) (PR #9903): split into sub-documents covering context strategies, LLM backends, resource types, A2A rename, container tool execution, scope chain resolution, cost/safety budgets, E2E workflow tests, code review examples, plugin architecture, TUI layout, persona system, reference/command input, session management, configuration, and TuiMaterializer integration.
* HAL 9000 has contributed the LLMTraceRepository data-integrity fix (PR #8185 / issue #7505): replaced the unconditional `session.commit()` in `LLMTraceRepository.save()` with a dual-path implementation that respects the UnitOfWork pattern — flushing only when an external session is provided, and flushing + committing + closing when operating standalone. This eliminates premature transaction commits, loss of rollback capability, and a docstring/implementation mismatch.
* HAL 9000 has contributed the ACMS Index Data Model and File Traversal Engine (PR #9664 / issue #9579): foundational data structures for indexed context entries with hot/warm/cold/archive storage tier classification, tag system, and a timeout-safe chunked file traversal engine for large projects with 10,000+ files.
* HAL 9000 has contributed the actor context list regex filter feature (PR #6741 / issue #6500): added an optional `[REGEX]` positional argument to `agents actor context list` command to filter named contexts by a regular expression pattern, including BDD test coverage and Robot Framework integration test with `--context-dir` support for isolation.
+45
View File
@@ -0,0 +1,45 @@
# Quick Start Guide
This quick start guide will walk you through creating a new project, registering a resource, running a plan, and applying changes with CleverAgents.
## Prerequisites
- Python 3.11+ and virtualenv
- Git
- A working CleverAgents installation (see development/testing.md for details)
## Install (local development)
```bash
python -m venv .venv
source .venv/bin/activate
pip install -e .[dev]
```
## Create a new project
```bash
# Create a new directory for your project
mkdir my-project && cd my-project
# Initialize a CleverAgents project (example command)
cleveragents init --name my-project
```
## Register a resource
Create a resource file under `resources/` (example YAML) and register it with the CLI or API.
## Plan and apply
```bash
# Create a plan using an action on your project
cleveragents plan use <action-name> --project my-project
# List plans to find the plan ID
cleveragents plan list
# Review the plan, then apply it by plan ID
cleveragents plan apply <plan-id>
```
## Troubleshooting
If you encounter issues running the examples above, consult `docs/development/testing.md` and the project README for local development tips.
+1 -1
View File
@@ -277,7 +277,7 @@ The following standards are integrated into the architecture:
[(<span style="color: magenta;"><span style="color: cyan;">--temperature</span>|<span style="color: yellow;">-t</span></span>) <span style="color: #66cc66;">&lt;TEMP&gt;</span>] [<span style="color: cyan;">--allow-rxpy-in-run-mode</span>]
[<span style="color: cyan;">--skill</span> <span style="color: #66cc66;">&lt;SKILL&gt;</span>]... <span style="color: #66cc66;">&lt;NAME&gt;</span> <span style="color: #66cc66;">&lt;PROMPT&gt;</span>
<span style="color: cyan; font-weight: 600;">agents</span> actor add <span style="color: cyan;">--config</span>|<span style="color: yellow;">-c</span> <span style="color: #66cc66;">&lt;FILE&gt;</span> [<span style="color: cyan;">--update</span>]
<span style="color: cyan; font-weight: 600;">agents</span> actor remove <span style="color: #66cc66;">&lt;NAME&gt;</span>
<span style="color: cyan; font-weight: 600;">agents</span> actor remove [<span style="color: cyan;">--format</span> <span style="color: #66cc66;">&lt;FORMAT&gt;</span>] <span style="color: #66cc66;">&lt;NAME&gt;</span>
<span style="color: cyan; font-weight: 600;">agents</span> actor list
<span style="color: cyan; font-weight: 600;">agents</span> actor show <span style="color: #66cc66;">&lt;NAME&gt;</span>
<span style="color: cyan; font-weight: 600;">agents</span> actor context remove [<span style="color: cyan;">--yes</span>|<span style="color: yellow;">-y</span>] (<span style="color: magenta;"><span style="color: cyan;">--all</span>|<span style="color: yellow;">-a</span>|<span style="color: #66cc66;">&lt;NAME&gt;</span></span>)
+5
View File
@@ -59,6 +59,11 @@ Feature: Actor CLI YAML-first alignment
When I run actor remove with namespaced name
Then the actor remove should succeed for namespaced name
Scenario: Actor remove outputs JSON format
Given an actor CLI runner
When I run actor remove with format json
Then the actor remove output should be valid JSON envelope
Scenario: Actor update outputs JSON format
Given an actor CLI runner
When I run actor update with format json
-9
View File
@@ -6,15 +6,6 @@ Feature: Actor context clear, remove, export, and import commands
Background:
Given a temporary context directory for actor context tests
@actor_context_list_regex
Scenario: List actor contexts filtered by regex
Given an actor context named "docs" exists
And an actor context named "notes" exists
When I run actor context list "docs"
Then the actor context list command should succeed
And the list output should contain "docs"
And the list output should not contain "notes"
# ── context clear ─────────────────────────────────────────
Scenario: Clear a named actor context
@@ -0,0 +1,38 @@
Feature: Cross-actor subgraph cycle detection reads actor_ref field
As a CleverAgents developer
I want the actor compiler to correctly detect cross-actor subgraph cycles
So that mutually-referencing actors raise SubgraphCycleError at compile time
Background:
Given the actor compiler is available
# ────────────────────────────────────────────────────────────
# Bug fix: actor_ref is a top-level NodeDefinition field
# ────────────────────────────────────────────────────────────
@tdd_issue @tdd_issue_1431
Scenario: Mutually-referencing actors raise SubgraphCycleError
Given actor "test/actor-a" has a subgraph node with actor_ref "test/actor-b"
And actor "test/actor-b" has a subgraph node with actor_ref "test/actor-a"
And a registry containing both actors
When I compile actor "test/actor-a" with the registry resolver
Then the compilation should raise SubgraphCycleError
And the cycle error message should mention "cycle"
@tdd_issue @tdd_issue_1431
Scenario: Non-cyclic subgraph reference compiles successfully
Given actor "test/actor-x" has a subgraph node with actor_ref "test/actor-y"
And actor "test/actor-y" has no subgraph nodes
And a registry containing both actors
When I compile actor "test/actor-x" with the registry resolver
Then the actor compilation should succeed
And the actor subgraph_refs should map "sub" to "test/actor-y"
@tdd_issue @tdd_issue_1431
Scenario: Subgraph node actor_ref is reflected in compiled metadata
Given actor "test/actor-p" has a subgraph node with actor_ref "test/actor-q"
And actor "test/actor-q" has no subgraph nodes
And a registry containing both actors
When I compile actor "test/actor-p" with the registry resolver
Then the actor compilation should succeed
And the compiled node "sub" should have subgraph set to "test/actor-q"
+1 -23
View File
@@ -16,18 +16,12 @@ Feature: LSP Resource Types
Examples:
| type_name |
| executable |
| lsp-server |
| lsp-workspace |
| lsp-document |
# ── User-addable flags ────────────────────────────────────────
Scenario: executable is user-addable for lsp_rt
Given the built-in executable YAML file for lsp_rt
When I load the YAML via schema for lsp_rt
Then the lsp_rt schema user_addable should be true
Scenario: lsp-server is user-addable for lsp_rt
Given the built-in lsp-server YAML file for lsp_rt
When I load the YAML via schema for lsp_rt
@@ -51,12 +45,6 @@ Feature: LSP Resource Types
Then the lsp_rt capability read should be true
And the lsp_rt capability write should be true
Scenario: executable has read-only capabilities for lsp_rt
Given the built-in executable YAML file for lsp_rt
When I load the YAML via schema for lsp_rt
Then the lsp_rt capability read should be true
And the lsp_rt capability write should be false
# ── Parent/child hierarchy ────────────────────────────────────
Scenario: lsp-server has lsp-workspace as child type for lsp_rt
@@ -81,12 +69,6 @@ Feature: LSP Resource Types
# ── Auto-discovery ────────────────────────────────────────────
Scenario: executable has auto-discovery from container-exec-env for lsp_rt
Given the built-in executable YAML file for lsp_rt
When I load the YAML via schema for lsp_rt
Then the lsp_rt auto_discovery should be present
And the lsp_rt auto_discovery trigger_types should contain "container-exec-env"
Scenario: lsp-workspace has auto-discovery from lsp-server for lsp_rt
Given the built-in lsp-workspace YAML file for lsp_rt
When I load the YAML via schema for lsp_rt
@@ -97,8 +79,7 @@ Feature: LSP Resource Types
Scenario: BUILTIN_NAMES includes all LSP types for lsp_rt
Given the ResourceTypeSpec BUILTIN_NAMES set for lsp_rt
Then BUILTIN_NAMES should contain "executable" for lsp_rt
And BUILTIN_NAMES should contain "lsp-server" for lsp_rt
Then BUILTIN_NAMES should contain "lsp-server" for lsp_rt
And BUILTIN_NAMES should contain "lsp-workspace" for lsp_rt
And BUILTIN_NAMES should contain "lsp-document" for lsp_rt
@@ -106,9 +87,6 @@ Feature: LSP Resource Types
Scenario: LSP types survive DB roundtrip after bootstrap for lsp_rt
Given a lsp_rt fresh in-memory resource registry with bootstrap
When I query the lsp_rt registry for type "executable"
Then the lsp_rt db type "executable" should exist
And the lsp_rt db type "executable" should have kind "physical"
When I query the lsp_rt registry for type "lsp-server"
Then the lsp_rt db type "lsp-server" should exist
When I query the lsp_rt registry for type "lsp-workspace"
+57
View File
@@ -337,6 +337,63 @@ def step_remove_namespaced_ok(context: Any) -> None:
)
@when("I run actor remove with format json")
def step_remove_format_json(context: Any) -> None:
with (
patch("cleveragents.cli.commands.actor._get_services") as mock_svc,
patch("cleveragents.cli.commands.actor._compute_actor_impact") as mock_impact,
):
mock_registry = MagicMock()
mock_service = MagicMock()
actor = _make_actor(
name="local/remove-json",
provider="json-provider",
model="gpt-json",
)
mock_registry.get_actor.return_value = actor
mock_impact.return_value = (2, 1, 3)
mock_svc.return_value = (mock_service, mock_registry)
context.result = context.runner.invoke(
actor_app,
["remove", actor.name, "--format", "json"],
)
context.mock_actor_registry = mock_registry
context.actor = actor
context.impact_counts = (2, 1, 3)
@then("the actor remove output should be valid JSON envelope")
def step_remove_json_valid(context: Any) -> None:
assert context.result.exit_code == 0
parsed = json.loads(context.result.output.strip())
assert _ENVELOPE_KEYS.issubset(parsed.keys())
assert parsed["command"] == f"agents actor remove {context.actor.name}"
assert parsed["status"] == "ok"
assert parsed["exit_code"] == 0
data = _unwrap_envelope(parsed)
assert isinstance(data, dict)
actor_data = data.get("actor_removed", {})
assert actor_data.get("name") == context.actor.name
assert actor_data.get("provider") == context.actor.provider
assert actor_data.get("model") == context.actor.model
impact = data.get("impact", {})
expected_sessions, expected_plans, expected_actions = context.impact_counts
assert impact.get("sessions") == expected_sessions
assert impact.get("active_plans") == expected_plans
assert impact.get("actions_referencing") == expected_actions
cleanup = data.get("cleanup", {})
assert cleanup.get("config") == "kept on disk"
assert cleanup.get("contexts") == "0 orphaned"
messages = parsed.get("messages", [])
assert messages, "expected messages in envelope"
first_message = messages[0]
assert first_message.get("level") == "ok"
assert "Actor removed" in first_message.get("text", "")
context.mock_actor_registry.remove_actor.assert_called_once_with(context.actor.name)
# ------------------------------------------------------------------
# Update with --format
# ------------------------------------------------------------------
@@ -135,7 +135,8 @@ def _subgraph_node(node_id: str, actor_ref: str = "") -> NodeDefinition:
type=NodeType.SUBGRAPH,
name=node_id.title(),
description=f"Subgraph {node_id}",
config={"actor_ref": actor_ref},
config={},
actor_ref=actor_ref if actor_ref else None,
)
+6 -3
View File
@@ -186,7 +186,8 @@ def step_given_subgraph_ref(context: Context, ref_name: str) -> None:
type=NodeType.SUBGRAPH,
name="Sub",
description="Subgraph",
config={"actor_ref": ref_name},
config={},
actor_ref=ref_name,
),
]
edges = [EdgeDefinition(from_node="main", to_node="sub")]
@@ -241,7 +242,8 @@ def step_given_outer_referencing_inner(
type=NodeType.SUBGRAPH,
name="Sub",
description="Subgraph",
config={"actor_ref": inner_name},
config={},
actor_ref=inner_name,
),
]
edges = [EdgeDefinition(from_node="main", to_node="sub")]
@@ -260,7 +262,8 @@ def step_given_resolver_cycle(context: Context, inner_name: str, back_ref: str)
type=NodeType.SUBGRAPH,
name="Child",
description="Back-ref",
config={"actor_ref": back_ref},
config={},
actor_ref=back_ref,
),
]
inner = _build_graph_config(inner_name, inner_nodes, [], "child", ["child"])
@@ -105,36 +105,6 @@ def step_create_json_file_with_name(context, name):
context.import_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
# ---------------------------------------------------------------------------
# When — list
# ---------------------------------------------------------------------------
@when('I run actor context list "{pattern}"')
def step_list_with_pattern(context, pattern):
context.result = context.runner.invoke(
actor_context_app,
[
"list",
pattern,
"--context-dir",
str(context.context_dir),
],
)
@when("I run actor context list without a pattern")
def step_list_without_pattern(context):
context.result = context.runner.invoke(
actor_context_app,
[
"list",
"--context-dir",
str(context.context_dir),
],
)
# ---------------------------------------------------------------------------
# When — clear
# ---------------------------------------------------------------------------
@@ -413,15 +383,6 @@ def step_roundtrip_import(context, name):
# ---------------------------------------------------------------------------
@then("the actor context list command should succeed")
def step_list_success(context):
assert context.result.exit_code == 0, (
f"Expected exit 0, got {context.result.exit_code}.\n"
f"stdout: {context.result.output}\n"
f"stderr: {getattr(context.result, 'stderr', '')}"
)
@then("the actor context clear command should succeed")
def step_clear_success(context):
assert context.result.exit_code == 0, (
@@ -553,18 +514,6 @@ def step_all_contexts_empty(context):
# ---------------------------------------------------------------------------
@then('the list output should contain "{text}"')
def step_list_output_contains(context, text):
output = context.result.output
assert text in output, f"Expected '{text}' in list output: {output}"
@then('the list output should not contain "{text}"')
def step_list_output_not_contains(context, text):
output = context.result.output
assert text not in output, f"Did not expect '{text}' in list output: {output}"
@then('the output should contain valid JSON with key "{key}"')
def step_output_json_key(context, key):
output = context.result.output
@@ -0,0 +1,201 @@
"""Step definitions for cross-actor subgraph cycle detection tests.
Tests for features/actor_subgraph_cycle_detection.feature validates that
the actor compiler correctly reads actor_ref from the top-level
NodeDefinition field (not from node.config) when detecting cross-actor
subgraph cycles.
This is the regression test for issue #1431: _detect_subgraph_cycles()
was reading node.config.get("actor_ref", "") instead of node.actor_ref,
causing cycle detection to always return an empty string and never detect
cross-actor cycles.
"""
from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from cleveragents.actor.compiler import (
SubgraphCycleError,
compile_actor,
)
from cleveragents.actor.schema import (
ActorConfigSchema,
ActorType,
EdgeDefinition,
NodeDefinition,
NodeType,
RouteDefinition,
)
# ────────────────────────────────────────────────────────────
# Helpers
# ────────────────────────────────────────────────────────────
def _make_graph_actor(
name: str,
nodes: list[NodeDefinition],
edges: list[EdgeDefinition],
entry_node: str,
exit_nodes: list[str],
) -> ActorConfigSchema:
"""Build a minimal valid GRAPH actor for testing."""
route = RouteDefinition(
nodes=nodes,
edges=edges,
entry_node=entry_node,
exit_nodes=exit_nodes,
)
return ActorConfigSchema(
name=name,
type=ActorType.GRAPH,
description=f"Test actor {name}",
provider="openai",
model="gpt-4",
route=route,
)
def _make_agent_node(node_id: str) -> NodeDefinition:
"""Build a minimal AGENT node."""
return NodeDefinition(
id=node_id,
type=NodeType.AGENT,
name=node_id.title(),
description=f"Agent {node_id}",
config={"agent": "default"},
)
def _make_subgraph_node(node_id: str, actor_ref: str) -> NodeDefinition:
"""Build a SUBGRAPH node using the top-level actor_ref field.
The actor_ref is set as a first-class field on NodeDefinition, NOT
inside config. This is the correct way to reference a subgraph actor.
"""
return NodeDefinition(
id=node_id,
type=NodeType.SUBGRAPH,
name=node_id.title(),
description=f"Subgraph {node_id}",
config={},
actor_ref=actor_ref,
)
# ────────────────────────────────────────────────────────────
# Given steps
# ────────────────────────────────────────────────────────────
@given("the actor compiler is available")
def step_compiler_available(context: Context) -> None:
"""Ensure the actor compiler module is importable."""
context.actor_registry: dict[str, ActorConfigSchema] = {}
@given('actor "{name}" has a subgraph node with actor_ref "{ref}"')
def step_actor_has_subgraph_ref(context: Context, name: str, ref: str) -> None:
"""Create a GRAPH actor with one agent node and one subgraph node."""
agent = _make_agent_node("start")
sub = _make_subgraph_node("sub", actor_ref=ref)
actor = _make_graph_actor(
name,
nodes=[agent, sub],
edges=[EdgeDefinition(from_node="start", to_node="sub")],
entry_node="start",
exit_nodes=["sub"],
)
context.actor_registry[name] = actor
context.actor_to_compile = name
@given('actor "{name}" has no subgraph nodes')
def step_actor_has_no_subgraph(context: Context, name: str) -> None:
"""Create a GRAPH actor with only an agent node (no subgraph references)."""
agent = _make_agent_node("leaf")
actor = _make_graph_actor(
name,
nodes=[agent],
edges=[],
entry_node="leaf",
exit_nodes=["leaf"],
)
context.actor_registry[name] = actor
@given("a registry containing both actors")
def step_registry_contains_both(context: Context) -> None:
"""Set up the resolver from the accumulated registry."""
registry = context.actor_registry
def resolver(actor_name: str) -> ActorConfigSchema | None:
return registry.get(actor_name)
context.resolver = resolver
# ────────────────────────────────────────────────────────────
# When steps
# ────────────────────────────────────────────────────────────
@when('I compile actor "{name}" with the registry resolver')
def step_compile_actor(context: Context, name: str) -> None:
"""Compile the named actor using the registry resolver."""
context.compile_error = None
context.compiled = None
actor = context.actor_registry[name]
try:
context.compiled = compile_actor(actor, actor_resolver=context.resolver)
except Exception as exc:
context.compile_error = exc
# ────────────────────────────────────────────────────────────
# Then steps
# ────────────────────────────────────────────────────────────
@then("the compilation should raise SubgraphCycleError")
def step_raises_cycle_error(context: Context) -> None:
"""Assert that compilation raised SubgraphCycleError."""
assert context.compile_error is not None, (
"Expected SubgraphCycleError but compilation succeeded"
)
assert isinstance(context.compile_error, SubgraphCycleError), (
f"Expected SubgraphCycleError, got {type(context.compile_error).__name__}: "
f"{context.compile_error}"
)
@then("the actor compilation should succeed")
def step_actor_compilation_succeeds(context: Context) -> None:
"""Assert that compilation succeeded without errors."""
assert context.compile_error is None, (
f"Expected compilation to succeed but got: {context.compile_error}"
)
assert context.compiled is not None
@then('the cycle error message should mention "{text}"')
def step_cycle_error_mentions(context: Context, text: str) -> None:
"""Assert that the cycle error message contains the given text."""
assert context.compile_error is not None
msg = str(context.compile_error)
assert text.lower() in msg.lower(), (
f"Expected '{text}' in error message, got: {msg}"
)
@then('the actor subgraph_refs should map "{node}" to "{ref}"')
def step_actor_subgraph_refs_map(context: Context, node: str, ref: str) -> None:
"""Assert that the compiled metadata maps the node to the expected actor ref."""
assert context.compiled is not None
refs = context.compiled.metadata.subgraph_refs
assert refs.get(node) == ref, (
f"Expected subgraph_refs[{node!r}] == {ref!r}, got {refs}"
)
+2 -1
View File
@@ -61,6 +61,7 @@ nav:
- Reference/Command Input & Sessions: tui/input-and-sessions.md
- Configuration, Key Bindings & Integration: tui/configuration-and-integration.md
- FAQ: faq.md
- Quick Start: quickstart.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- Reference: reference/
@@ -93,7 +94,7 @@ nav:
- ADR-025 Observability & Logging: adr/ADR-025-observability-and-logging.md
- ADR-026 Agent-to-Agent Protocol (A2A): adr/ADR-026-agent-client-protocol.md
- ADR-027 Language Server Protocol (LSP) Integration: adr/ADR-027-language-server-protocol.md
- ADR-028 Agent Skills Standard (AgentSkills.io): adr/ADR-028-agent-skills-standard.md
- ADR-028 Skill/Tool Abstraction Definition: adr/ADR-028-agent-skills-standard.md
- ADR-029 Model Context Protocol (MCP) Adoption: adr/ADR-029-model-context-protocol.md
- ADR-030 Skill Abstraction Definition: adr/ADR-030-skill-abstraction-definition.md
- ADR-031 Actor Abstraction Definition: adr/ADR-031-actor-abstraction-definition.md
+12
View File
@@ -42,3 +42,15 @@ Reject LLM Actor Compilation
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-compiler-expected-fail
Should Contain ${result.stdout} GRAPH
Detect Cross-Actor Subgraph Cycle Via actor_ref Field
[Documentation] Verify that mutually-referencing actors raise SubgraphCycleError.
... This is the regression test for issue #1431: _detect_subgraph_cycles()
... was reading node.config.get("actor_ref") instead of node.actor_ref,
... causing cycle detection to silently fail.
[Tags] slow
${result}= Run Process ${PYTHON} ${HELPER} cycle-detect dummy cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-compiler-cycle-detected
+18
View File
@@ -0,0 +1,18 @@
*** Settings ***
Documentation Integration tests for actor remove CLI format output
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_actor_remove_cli.py
*** Test Cases ***
Actor Remove Format JSON Emits Spec Envelope
[Documentation] Verify that ``actor remove --format json`` emits a spec-compliant envelope
[Tags] actor_remove_cli_format
${result}= Run Process ${PYTHON} ${HELPER} remove-json cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-remove-json-format-ok
-32
View File
@@ -81,38 +81,6 @@ List Context Files
[Teardown] Cleanup Test Directory
Actor Context List Supports Regex Filter
[Documentation] Verify actor context list supports optional regex filtering
... Uses --context-dir to point at a directory of named actor
... context subdirectories so the test is self-contained and
... does not depend on project-context state.
[Setup] Setup Test Directory
# Create a temporary context directory with two named actor context subdirs
${ctx_dir}= Set Variable ${TEST_DIR}${/}actor_contexts
Create Directory ${ctx_dir}
Create Directory ${ctx_dir}${/}test-context
Create Directory ${ctx_dir}${/}utils-context
# Filter by "test" — should match test-context but not utils-context
${result}= Run Process ${PYTHON} -m cleveragents actor context list test
... --context-dir ${ctx_dir}
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} test-context
Should Not Contain ${result.stdout} utils-context
# Filter by "nomatch" — no contexts match, should print "No contexts found."
${result}= Run Process ${PYTHON} -m cleveragents actor context list nomatch
... --context-dir ${ctx_dir}
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} No contexts found.
[Teardown] Cleanup Test Directory
Show Context Content
[Documentation] Test context show command
[Setup] Initialize Test Project With Context
+76
View File
@@ -66,6 +66,82 @@ def main() -> int:
print(f"actor-compiler-fail: {exc}")
return 1
if command == "cycle-detect":
# Test cross-actor cycle detection using actor_ref top-level field.
# Creates two mutually-referencing actors and verifies SubgraphCycleError
# is raised, confirming the fix for issue #1431.
from cleveragents.actor.compiler import SubgraphCycleError
from cleveragents.actor.schema import (
ActorType,
EdgeDefinition,
NodeDefinition,
NodeType,
RouteDefinition,
)
def _make_actor(
name: str, subgraph_ref: str | None = None
) -> ActorConfigSchema:
if subgraph_ref is not None:
nodes = [
NodeDefinition(
id="start",
type=NodeType.AGENT,
name="Start",
description="Start",
config={"agent": "default"},
),
NodeDefinition(
id="sub",
type=NodeType.SUBGRAPH,
name="Sub",
description="Subgraph",
config={},
actor_ref=subgraph_ref,
),
]
edges = [EdgeDefinition(from_node="start", to_node="sub")]
entry, exits = "start", ["sub"]
else:
nodes = [
NodeDefinition(
id="leaf",
type=NodeType.AGENT,
name="Leaf",
description="Leaf",
config={"agent": "default"},
)
]
edges = []
entry, exits = "leaf", ["leaf"]
route = RouteDefinition(
nodes=nodes, edges=edges, entry_node=entry, exit_nodes=exits
)
return ActorConfigSchema(
name=name,
type=ActorType.GRAPH,
description=f"Test actor {name}",
model="gpt-4",
route=route,
)
actor_a = _make_actor("test/actor-a", subgraph_ref="test/actor-b")
actor_b = _make_actor("test/actor-b", subgraph_ref="test/actor-a")
registry = {"test/actor-a": actor_a, "test/actor-b": actor_b}
try:
compile_actor(actor_a, actor_resolver=registry.get)
print(
"actor-compiler-cycle-not-detected: FAIL — expected SubgraphCycleError"
)
return 1
except SubgraphCycleError as exc:
print(f"actor-compiler-cycle-detected: {exc}")
return 0
except Exception as exc:
print(f"actor-compiler-unexpected-error: {exc}")
return 1
print(f"Unknown command: {command}")
return 1
+164
View File
@@ -0,0 +1,164 @@
"""Helper script for Robot integration tests covering ``actor remove --format`` output.
Exercises the real ``agents`` CLI via subprocess no mocking of any kind.
A test actor is seeded via ``agents actor add``, then removed via
``agents actor remove --format json``, and the resulting JSON envelope is
validated against the spec.
Usage::
python helper_actor_remove_cli.py <command>
Where *command* is one of:
- ``remove-json`` seed an actor, remove it with ``--format json``, validate envelope
"""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
# Ensure src is importable when run from workspace root
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
# Ensure robot/ is on the import path for helper_e2e_common.
_ROBOT = str(Path(__file__).resolve().parent)
if _ROBOT not in sys.path:
sys.path.insert(0, _ROBOT)
from helper_e2e_common import cleanup_workspace, run_cli, setup_workspace # noqa: E402
_ACTOR_NAME = "local/robot-remove-actor"
_ACTOR_CONFIG: dict[str, object] = {
"name": _ACTOR_NAME,
"provider": "openai",
"model": "gpt-4",
}
def _write_actor_config(workspace: str) -> str:
"""Write actor config JSON to a temp file and return its path."""
config_path = os.path.join(workspace, "robot_remove_actor.json")
with open(config_path, "w", encoding="utf-8") as fh:
json.dump(_ACTOR_CONFIG, fh)
return config_path
def test_remove_format_json() -> None:
"""Seed an actor via the real CLI, remove it with ``--format json``.
Validates the resulting JSON envelope against the spec.
"""
workspace = setup_workspace(prefix="robot_actor_remove_")
try:
config_path = _write_actor_config(workspace)
# Step 1: Add the actor using the real CLI.
add_result = run_cli(
"actor",
"add",
_ACTOR_NAME,
"--config",
config_path,
workspace=workspace,
)
assert add_result.returncode == 0, (
f"actor add failed (rc={add_result.returncode}):\n"
f"stdout: {add_result.stdout}\nstderr: {add_result.stderr}"
)
# Step 2: Remove the actor with --format json using the real CLI.
remove_result = run_cli(
"actor",
"remove",
_ACTOR_NAME,
"--format",
"json",
workspace=workspace,
)
assert remove_result.returncode == 0, (
f"actor remove --format json failed (rc={remove_result.returncode}):\n"
f"stdout: {remove_result.stdout}\nstderr: {remove_result.stderr}"
)
# Step 3: Parse and validate the JSON envelope.
output = remove_result.stdout.strip()
assert output, (
f"actor remove --format json produced no output.\n"
f"stderr: {remove_result.stderr}"
)
payload = json.loads(output)
assert payload["command"] == f"agents actor remove {_ACTOR_NAME}", (
f"Unexpected command field: {payload.get('command')!r}"
)
assert payload["status"] == "ok", (
f"Unexpected status: {payload.get('status')!r}"
)
assert payload["exit_code"] == 0, (
f"Unexpected exit_code: {payload.get('exit_code')!r}"
)
data = payload["data"]
removed = data.get("actor_removed", {})
assert removed.get("name") == _ACTOR_NAME, (
f"actor_removed.name mismatch: {removed.get('name')!r}"
)
assert removed.get("provider") == _ACTOR_CONFIG["provider"], (
f"actor_removed.provider mismatch: {removed.get('provider')!r}"
)
assert removed.get("model") == _ACTOR_CONFIG["model"], (
f"actor_removed.model mismatch: {removed.get('model')!r}"
)
impact = data.get("impact", {})
assert "sessions" in impact, f"Missing 'sessions' in impact: {impact}"
assert "active_plans" in impact, f"Missing 'active_plans' in impact: {impact}"
assert "actions_referencing" in impact, (
f"Missing 'actions_referencing' in impact: {impact}"
)
cleanup = data.get("cleanup", {})
assert cleanup.get("config") == "kept on disk", (
f"cleanup.config mismatch: {cleanup.get('config')!r}"
)
assert "contexts" in cleanup, f"Missing 'contexts' in cleanup: {cleanup}"
messages = payload.get("messages", [])
assert messages, f"Expected non-empty messages list, got: {messages}"
assert messages[0].get("level") == "ok", (
f"Unexpected message level: {messages[0].get('level')!r}"
)
assert "Actor removed" in messages[0].get("text", ""), (
f"Expected 'Actor removed' in message text: {messages[0].get('text')!r}"
)
print("actor-remove-json-format-ok")
finally:
cleanup_workspace(workspace)
def main() -> None:
command = sys.argv[1] if len(sys.argv) > 1 else "remove-json"
dispatch: dict[str, object] = {
"remove-json": test_remove_format_json,
}
handler = dispatch.get(command)
if handler is None:
print(f"Unknown command: {command}", file=sys.stderr)
sys.exit(1)
assert callable(handler)
handler()
if __name__ == "__main__":
main()
+18 -19
View File
@@ -15,26 +15,30 @@ def _import_lsp_types() -> None:
LSP_RESOURCE_TYPES,
)
assert len(LSP_RESOURCE_TYPES) == 4, f"Expected 4, got {len(LSP_RESOURCE_TYPES)}"
assert len(LSP_RESOURCE_TYPES) == 3, f"Expected 3, got {len(LSP_RESOURCE_TYPES)}"
names = [t["name"] for t in LSP_RESOURCE_TYPES]
for expected in ("executable", "lsp-server", "lsp-workspace", "lsp-document"):
for expected in ("lsp-server", "lsp-workspace", "lsp-document"):
assert expected in names, f"{expected} not in LSP_RESOURCE_TYPES: {names}"
print("import-lsp-types-ok")
def _check_builtin_names() -> None:
"""Verify all 4 LSP types are in BUILTIN_NAMES."""
"""Verify all 3 LSP types are in BUILTIN_NAMES."""
from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
for name in ("executable", "lsp-server", "lsp-workspace", "lsp-document"):
for name in ("lsp-server", "lsp-workspace", "lsp-document"):
assert name in ResourceTypeSpec.BUILTIN_NAMES, f"{name} not in BUILTIN_NAMES"
assert "executable" not in ResourceTypeSpec.BUILTIN_NAMES, (
"executable should not be in BUILTIN_NAMES (not a spec-defined type)"
)
print("check-builtin-names-ok")
def _db_roundtrip() -> None:
"""Bootstrap in-memory DB and verify all 4 LSP types are retrievable."""
"""Bootstrap in-memory DB and verify all 3 LSP types are retrievable."""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@@ -48,7 +52,7 @@ def _db_roundtrip() -> None:
factory = sessionmaker(bind=engine, expire_on_commit=False)
svc = ResourceRegistryService(session_factory=factory)
for name in ("executable", "lsp-server", "lsp-workspace", "lsp-document"):
for name in ("lsp-server", "lsp-workspace", "lsp-document"):
spec = svc.show_type(name)
assert spec is not None, f"{name} not found after bootstrap"
assert str(spec.resource_kind) == "physical", (
@@ -85,32 +89,27 @@ def _check_hierarchy() -> None:
assert "lsp-workspace" in types["lsp-document"]["parent_types"], (
"lsp-document missing lsp-workspace parent"
)
# executable has no children
assert types["executable"]["child_types"] == [], (
"executable should have no children"
)
print("check-hierarchy-ok")
def _check_auto_discovery() -> None:
"""Verify executable has auto_discovery with trigger_types."""
"""Verify lsp-workspace has auto_discovery with trigger_types."""
from cleveragents.application.services._resource_registry_lsp import (
LSP_RESOURCE_TYPES,
)
types = {t["name"]: t for t in LSP_RESOURCE_TYPES}
types = {t["name"] for t in LSP_RESOURCE_TYPES}
# executable has auto_discovery
ad = types["executable"].get("auto_discovery")
assert ad is not None, "executable missing auto_discovery"
assert "container-exec-env" in ad.get("trigger_types", []), (
"executable missing container-exec-env trigger"
# executable should not be present
assert "executable" not in types, (
"executable should not be in LSP_RESOURCE_TYPES (not a spec-defined type)"
)
assert ad.get("lazy") is True, "executable auto_discovery should be lazy"
lsp_types = {t["name"]: t for t in LSP_RESOURCE_TYPES}
# lsp-workspace has auto_discovery from lsp-server
ad_ws = types["lsp-workspace"].get("auto_discovery")
ad_ws = lsp_types["lsp-workspace"].get("auto_discovery")
assert ad_ws is not None, "lsp-workspace missing auto_discovery"
assert "lsp-server" in ad_ws.get("trigger_types", []), (
"lsp-workspace missing lsp-server trigger"
+8 -3
View File
@@ -137,7 +137,7 @@ def _map_node(node: NodeDefinition) -> lg_nodes.NodeConfig:
config.get("function") if node.type == NodeType.CONDITIONAL else None
),
tools=config.get("tools", []) if node.type == NodeType.TOOL else [],
subgraph=(config.get("actor_ref") if node.type == NodeType.SUBGRAPH else None),
subgraph=(node.actor_ref if node.type == NodeType.SUBGRAPH else None),
metadata=dict(config),
)
@@ -187,6 +187,11 @@ def _detect_subgraph_cycles(
"""Detect cycles in subgraph references across actors.
Returns list of actor names forming a cycle, or empty list.
The actor reference is read from the top-level actor_ref field on
NodeDefinition, not from node.config. Reading from config
would always return an empty string because actor_ref is stored as a
first-class field on the schema model.
"""
if resolver is None:
return []
@@ -194,7 +199,7 @@ def _detect_subgraph_cycles(
for node in route_nodes:
if node.type != NodeType.SUBGRAPH:
continue
ref_name = node.config.get("actor_ref", "")
ref_name = node.actor_ref or ""
if not ref_name:
continue
if ref_name in visited:
@@ -290,7 +295,7 @@ def compile_actor(
all_lsp_bindings.extend(bindings)
if node_def.type == NodeType.SUBGRAPH:
ref = node_def.config.get("actor_ref", "")
ref = node_def.actor_ref or ""
if ref:
subgraph_refs[node_def.id] = ref
@@ -1,15 +1,14 @@
"""LSP resource type definitions for the Resource Registry.
Contains the built-in LSP-related type entries (``executable``,
``lsp-server``, ``lsp-workspace``, ``lsp-document``) that are appended
to ``BUILTIN_TYPES`` in ``_resource_registry_data``.
Contains the built-in LSP-related type entries (``lsp-server``,
``lsp-workspace``, ``lsp-document``) that are appended to
``BUILTIN_TYPES`` in ``_resource_registry_data``.
These types bridge the resource system with the LSP runtime, allowing
actors to discover available language tooling and the LSP system to know
which resources it operates on.
Spec reference: docs/adr/ADR-039-container-resource-types.md (executable),
docs/adr/ADR-040-lsp-resource-types.md (lsp-server,
Spec reference: docs/adr/ADR-040-lsp-resource-types.md (lsp-server,
lsp-workspace, lsp-document)
.. note::
@@ -28,52 +27,11 @@ __all__ = ["LSP_RESOURCE_TYPES"]
# === LSP Resource Types (#832) ===
#
# These types model language tooling infrastructure in the resource DAG.
# - executable: system binary / interpreter / LSP server binary
# - lsp-server: a configured or running LSP server instance
# - lsp-workspace: workspace root tracked by an LSP server
# - lsp-document: text document tracked by an LSP server
LSP_RESOURCE_TYPES: list[dict[str, Any]] = [
# ── executable ───────────────────────────────────────────────
{
"name": "executable",
"description": (
"System executable (language runtime, compiler, LSP server "
"binary). Discovered from container or host PATH."
),
"resource_kind": "physical",
"sandbox_strategy": "none",
"user_addable": True,
"built_in": True,
"cli_args": [
{
"name": "path",
"type": "string",
"required": True,
"description": "Absolute path to the executable",
},
{
"name": "version",
"type": "string",
"required": False,
"description": "Executable version if detectable",
},
],
"parent_types": ["container-exec-env", "fs-directory"],
"child_types": [],
"handler": "cleveragents.resource.handlers.executable:ExecutableHandler",
"auto_discovery": {
"trigger_types": ["container-exec-env", "fs-directory"],
"scan_paths": [],
"lazy": True,
},
"capabilities": {
"read": True,
"write": False,
"sandbox": False,
"checkpoint": False,
},
},
# ── lsp-server ───────────────────────────────────────────────
{
"name": "lsp-server",
+51 -1
View File
@@ -816,12 +816,28 @@ def update(
@app.command()
def remove(name: Annotated[str, typer.Argument(help="Actor name to remove")]) -> None:
def remove(
name: Annotated[str, typer.Argument(help="Actor name to remove")],
fmt: Annotated[
str,
typer.Option("--format", "-f", help=_FORMAT_HELP),
] = OutputFormat.RICH.value,
) -> None:
"""Remove a custom actor.
Specify the namespaced name (e.g. ``local/my-actor``).
"""
# Validate --format argument first; fail fast before any side effects.
fmt_value = fmt.lower()
_valid_formats = {f.value for f in OutputFormat}
if fmt_value not in _valid_formats:
raise typer.BadParameter(
f"Invalid format {fmt!r}. "
f"Supported values: {', '.join(sorted(_valid_formats))}",
param_hint="'--format'",
)
service, registry = _get_services()
try:
# Get actor details before removal for display
@@ -844,6 +860,40 @@ def remove(name: Annotated[str, typer.Argument(help="Actor name to remove")]) ->
else:
service.remove_actor(name)
command_name = f"agents actor remove {name}"
payload = {
"actor_removed": {
"name": name,
"provider": actor_provider,
"model": actor_model,
},
"impact": {
"sessions": session_count,
"active_plans": active_plan_count,
"actions_referencing": action_count,
},
"cleanup": {
"config": "kept on disk",
# NOTE: context-cleanup count is deferred; always 0 for now.
# Follow-up: implement dynamic orphaned-context detection.
"contexts": "0 orphaned",
},
}
messages = [{"level": "ok", "text": "Actor removed"}]
if fmt_value != OutputFormat.RICH.value:
rendered = format_output(
payload,
fmt_value,
command=command_name,
status="ok",
exit_code=0,
messages=messages,
)
if rendered:
console.print(rendered)
return
# Display Actor Removed panel
actor_info = (
f"[cyan bold]Name:[/cyan bold] {name}\n"
@@ -12,7 +12,6 @@ from __future__ import annotations
import hashlib
import json
import re
import shutil
from pathlib import Path
from typing import Annotated, Any
@@ -32,46 +31,6 @@ console = Console()
_FORMAT_HELP = "Output format: json, yaml, plain, table, rich, or color (default: rich)"
@app.command("list")
def context_list(
regex: Annotated[
str | None,
typer.Argument(help="Optional regex filter for context names"),
] = None,
context_dir: Annotated[
Path | None,
typer.Option(
"--context-dir",
help="Directory where contexts are stored",
resolve_path=True,
),
] = None,
) -> None:
"""List named actor contexts, optionally filtered by a regular expression."""
pattern: re.Pattern[str] | None = None
if regex is not None:
try:
pattern = re.compile(regex)
except re.error as exc:
console.print(f"[red]Error:[/red] Invalid regex pattern: {exc}")
raise typer.Abort() from exc
base = _default_context_base(context_dir)
context_names = _list_context_names(base)
if pattern is not None:
context_names = [name for name in context_names if pattern.search(name)]
if not context_names:
typer.echo("No contexts found.")
return
for name in context_names:
typer.echo(name)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
+7 -34
View File
@@ -9,7 +9,6 @@ Deprecated alias: ``agents context <subcommand>`` (emits deprecation warning)
from __future__ import annotations
import re
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Any
@@ -397,10 +396,6 @@ def context_rm_deprecated(
@app.command("list")
def context_list(
regex: Annotated[
str | None,
typer.Argument(help="Optional regex filter for context names"),
] = None,
context_dir: Annotated[
Path | None,
typer.Option(
@@ -414,14 +409,6 @@ def context_list(
Or list named contexts in a directory.
"""
pattern: re.Pattern[str] | None = None
if regex is not None:
try:
pattern = re.compile(regex)
except re.error as exc:
console.print(f"[red]Error:[/red] Invalid regex pattern: {exc}")
raise typer.Abort() from exc
if context_dir is not None:
# List named contexts in the given directory
ctx_base = context_dir
@@ -430,8 +417,6 @@ def context_list(
return
context_dirs = sorted([d.name for d in ctx_base.iterdir() if d.is_dir()])
if pattern is not None:
context_dirs = [name for name in context_dirs if pattern.search(name)]
if not context_dirs:
typer.echo("No contexts found.")
@@ -466,36 +451,24 @@ def context_list(
console.print("Use 'agents actor context add <path>' to add files.")
return
# Filter and normalize entries
filtered_entries: list[tuple[str, str, str, str]] = []
# Display context files
table = Table(title=f"Context Files ({len(context_files)} total)")
table.add_column("File Path", style="cyan")
table.add_column("Type", style="green")
table.add_column("Size", style="magenta")
table.add_column("Added", style="yellow")
for file_info in context_files:
path_text, type_label, size, added_at, _ = _normalize_context_entry(
file_info
)
display_name = Path(path_text).name if path_text else ""
if pattern is not None and not pattern.search(display_name):
continue
if isinstance(size, str):
size_str = size
else:
size_str = f"{size:,} bytes" if size > 0 else "0 bytes"
filtered_entries.append((display_name, type_label, size_str, added_at))
if not filtered_entries:
console.print("[yellow]No files matched the pattern.[/yellow]")
return
# Display context files
table = Table(title=f"Context Files ({len(filtered_entries)} total)")
table.add_column("File Path", style="cyan")
table.add_column("Type", style="green")
table.add_column("Size", style="magenta")
table.add_column("Added", style="yellow")
for display_name, type_label, size_str, added_at in filtered_entries:
table.add_row(
display_name,
type_label,
@@ -125,7 +125,6 @@ BUILTIN_TYPE_NAMES: frozenset[str] = frozenset(
"submodule",
"symlink",
# LSP resource types (#832)
"executable",
"lsp-server",
"lsp-workspace",
"lsp-document",