chore: resolve merge conflicts and apply cosmetic fixes
CI / push-validation (push) Successful in 29s
CI / helm (push) Successful in 39s
CI / build (push) Successful in 1m9s
CI / lint (push) Successful in 1m23s
CI / typecheck (push) Successful in 1m34s
CI / quality (push) Successful in 1m39s
CI / security (push) Successful in 1m46s
CI / e2e_tests (push) Successful in 52s
CI / benchmark-regression (push) Failing after 40s
CI / integration_tests (push) Successful in 5m6s
CI / unit_tests (push) Successful in 6m7s
CI / docker (push) Successful in 1m28s
CI / coverage (push) Successful in 10m46s
CI / status-check (push) Successful in 7s
CI / benchmark-publish (push) Failing after 45m49s
CI / helm (pull_request) Successful in 45s
CI / build (pull_request) Successful in 1m8s
CI / lint (pull_request) Successful in 1m45s
CI / quality (pull_request) Successful in 1m39s
CI / security (pull_request) Successful in 2m11s
CI / typecheck (pull_request) Successful in 2m21s
CI / push-validation (pull_request) Successful in 22s
CI / integration_tests (pull_request) Successful in 4m19s
CI / unit_tests (pull_request) Successful in 5m5s
CI / docker (pull_request) Successful in 1m47s
CI / coverage (pull_request) Successful in 11m53s
CI / status-check (pull_request) Successful in 3s

- Resolve CHANGELOG.md conflict to include both entries, referencing issue #8164 instead of PR #11161
- Resolve CONTRIBUTORS.md conflict, fix leading space in new entry
- Remove duplicate create_sequence_node function (dead code)
Merges master into feat/structural-output-validation branch
This commit was merged in pull request #11161.
This commit is contained in:
2026-05-14 18:14:48 +00:00
8 changed files with 431 additions and 54 deletions
+8 -1
View File
@@ -54,7 +54,14 @@ Changed `wf10_batch.robot` to be less likely to create files, and
## [Unreleased]
- **Structural Component Output Validation** (#11161): Replaces exact character matching with structural component checking for output validation. Implements three validators covering plan tree output, decision CLI dicts, and structured session snapshots. The `validate_plan_tree` function validates node dicts for required keys (`decision_id`, `type`, `sequence`, `question`, `children`), ULID format, correct types, and sibling ordering. The `validate_decision_dict` function validates decision CLI output against the `Decision.as_cli_dict()` schema with field presence, type, ULID pattern, confidence range [0..1], and boolean field checks. The `validate_structured_output` function validates the StructuredOutput envelope for `command`, `session_id` (ULID), status membership, `exit_code`, and elements integrity. A unified dispatcher (`validate_structured_component_output`) enables routing by target_type. BDD test coverage added in `features/structural_validation.feature`. [Epic #8137](https://git.cleverthis.com/cleveragents/cleveragents-core/issues/8137)
- **Structural Component Output Validation** (#8164): Replaces exact character matching with structural component checking for output validation. Implements three validators covering plan tree output, decision CLI dicts, and structured session snapshots. The `validate_plan_tree` function validates node dicts for required keys (`decision_id`, `type`, `sequence`, `question`, `children`), ULID format, correct types, and sibling ordering. The `validate_decision_dict` function validates decision CLI output against the `Decision.as_cli_dict()` schema with field presence, type, ULID pattern, confidence range [0..1], and boolean field checks. The `validate_structured_output` function validates the StructuredOutput envelope for `command`, `session_id` (ULID), status membership, `exit_code`, and elements integrity. A unified dispatcher (`validate_structured_component_output`) enables routing by target_type. BDD test coverage added in `features/structural_validation.feature`. [Epic #8137](https://git.cleverthis.com/cleveragents/cleveragents-core/issues/8137)
- **Fixed `agents actor add --config` crash with nested `actors:` map and `config.actor` combined shorthand** (#11189): The
CLI command `agents actor add --config` now correctly parses spec-canonical YAML using the
nested `actors:` map format with `config.actor: "provider/model"` combined shorthand.
Fixed two defects in `ActorConfiguration._extract_v3_actor()`: `type` is now detected at
the actor-entry level (sibling of `config`), and `config.actor` is parsed as fallback
when separate `provider`/`model` keys are absent. Added validation to reject malformed
combined values with empty provider or model halves.
- **`task-implementor` posts work-started notification comments** (#11031): Both
the `issue_impl` and `pr_fix` procedures now post an informational "work
+2 -1
View File
@@ -24,7 +24,8 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed the plugin entry point security hardening fix (#7476): enforced entry point allowlist validation before importing plugin modules to prevent malicious plugin loading.
* HAL 9000 has contributed the benchmark workflow separation (#9040): moved the benchmark-regression job out of the default PR workflow into a dedicated scheduled workflow, reducing median PR CI turnaround time from 99-132 minutes to under 30 minutes.
* HAL 9000 has contributed the plan tree JSON/YAML command envelope fix (#9163): wrapped `agents plan tree --format json/yaml` output in the spec-required command envelope structure, added summary statistics, decision_ids mapping, child_plans list, and accurate timing measurement.
* HAL 9000 contributed Structural Component Output Validation (PR #11161): implemented `validate_plan_tree`, `validate_decision_dict`, `validate_structured_output`, and `validate_structured_component_output` validators that replace exact-character matching with structural schema checking for plan tree nodes, decision CLI dictionaries, and structured session output envelopes.
* HAL 9000 contributed Structural Component Output Validation (PR #11161 / issue #8164): implemented `validate_plan_tree`, `validate_decision_dict`, `validate_structured_output`, and `validate_structured_component_output` validators that replace exact-character matching with structural schema checking for plan tree nodes, decision CLI dictionaries, and structured session output envelopes.
* HAMZA KHYARI has contributed the ACMS execute-phase context assembler project-level hot_max_tokens fix (PR #11036 / issue #11035): added `_resolve_effective_budget()` method that reads each linked project's `settings.hot_max_tokens` and uses the maximum override value as the pipeline budget instead of the hardcoded global 16K default.
* This project was made possible thanks to considerable donation of time, money, and resources by CleverThis, Inc.
* HAL 9000 has contributed automated bug fixes, CLI output formatting improvements, and ongoing maintenance as part of the CleverAgents automation system.
* HAL 9000 has contributed the file edit encoding parameter fix (PR #8258 / issue #7559).
@@ -271,3 +271,62 @@ Feature: Actor add CLI validates v3 YAML via ActorConfigSchema
When I run the actor add command for "local/bad-edge-ref" with the prepared config file
Then v3actor the command should fail
And v3actor the error message should contain "not found"
# ────────────────────────────────────────────────────────────
# Nested actors map: combined config.actor field (bug #11189)
# ────────────────────────────────────────────────────────────
@tdd_issue @tdd_issue_11189
Scenario: Register LLM actor using nested actors map with combined config.actor field
Given a nested LLM actor YAML using combined config.actor field with name "local/nested-combined-llm"
When I run the actor add command for "local/nested-combined-llm" with the prepared config file
Then v3actor the command should succeed
And v3actor the actor should be registered with provider "anthropic" and model "claude-sonnet-4-5"
Scenario: Explicit config.provider takes precedence over provider derived from config.actor
Given a nested LLM actor YAML with explicit config.provider overriding config.actor with name "local/explicit-provider-llm"
When I run the actor add command for "local/explicit-provider-llm" with the prepared config file
Then v3actor the command should succeed
And v3actor the actor should be registered with provider "explicit-provider"
And v3actor the actor should be registered with model "gpt-4"
Scenario: Nested actors map with no provider or model still raises validation error
Given a nested LLM actor YAML with no provider or model fields with name "local/no-provider-nested-llm"
When I run the actor add command for "local/no-provider-nested-llm" with the prepared config file
Then v3actor the command should fail
And v3actor the error message should contain "provider"
Scenario: Explicit config.model takes precedence over model derived from config.actor
Given a nested LLM actor YAML with explicit config.model overriding config.actor with name "local/explicit-model-llm"
When I run the actor add command for "local/explicit-model-llm" with the prepared config file
Then v3actor the command should succeed
And v3actor the actor should be registered with provider "anthropic"
And v3actor the actor should be registered with model "explicit-model"
# ────────────────────────────────────────────────────────────
# Malformed config.actor edge cases
# ────────────────────────────────────────────────────────────
Scenario: Combined config.actor with empty model half is treated as absent
Given a nested LLM actor YAML with combined config.actor "provider/" and name "local/empty-model-actor"
When I run the actor add command for "local/empty-model-actor" with the prepared config file
Then v3actor the command should fail
And v3actor the error message should contain "provider"
Scenario: Combined config.actor with empty provider half is treated as missing provider
Given a nested LLM actor YAML with combined config.actor "/model" and name "local/empty-provider-actor"
When I run the actor add command for "local/empty-provider-actor" with the prepared config file
Then v3actor the command should fail
And v3actor the error message should contain "provider"
Scenario: Combined config.actor without slash delimiter is treated as absent
Given a nested LLM actor YAML with combined config.actor "noslash" and name "local/noslash-actor"
When I run the actor add command for "local/noslash-actor" with the prepared config file
Then v3actor the command should fail
And v3actor the error message should contain "provider"
Scenario: Combined config.actor with both halves empty is treated as absent
Given a nested LLM actor YAML with combined config.actor "/" and name "local/slash-only-actor"
When I run the actor add command for "local/slash-only-actor" with the prepared config file
Then v3actor the command should fail
And v3actor the error message should contain "provider"
@@ -440,15 +440,22 @@ def step_run_actor_add(context: Context, name: str) -> None:
assert result.exception is None, f"Unexpected exception: {result.exception}"
context.command_error = None
context.actor_registered = True
# Verify registry was called and extract the actor_type from the config
# Verify registry was called and extract actor metadata from the config blob
if mock_registry.upsert_actor.called:
call_kwargs = mock_registry.upsert_actor.call_args
blob = call_kwargs.kwargs.get("config_blob") if call_kwargs.kwargs else None
context.registered_actor_type = (
blob.get("type") if isinstance(blob, dict) else None
)
if isinstance(blob, dict):
context.registered_actor_type = blob.get("type")
context.registered_actor_provider = blob.get("provider")
context.registered_actor_model = blob.get("model")
else:
context.registered_actor_type = None
context.registered_actor_provider = None
context.registered_actor_model = None
else:
context.registered_actor_type = None
context.registered_actor_provider = None
context.registered_actor_model = None
else:
assert isinstance(result.exception, SystemExit), (
f"Expected SystemExit, got: {result.exception}"
@@ -456,6 +463,8 @@ def step_run_actor_add(context: Context, name: str) -> None:
context.command_error = result.output
context.actor_registered = False
context.registered_actor_type = None
context.registered_actor_provider = None
context.registered_actor_model = None
@when('I run the actor update command for "{name}" with the prepared config file')
@@ -532,15 +541,22 @@ def step_run_actor_update(context: Context, name: str) -> None:
assert result.exception is None, f"Unexpected exception: {result.exception}"
context.command_error = None
context.actor_registered = True
# Verify registry was called and extract the actor_type from the config
# Verify registry was called and extract actor metadata from the config blob
if mock_registry.upsert_actor.called:
call_kwargs = mock_registry.upsert_actor.call_args
blob = call_kwargs.kwargs.get("config_blob") if call_kwargs.kwargs else None
context.registered_actor_type = (
blob.get("type") if isinstance(blob, dict) else None
)
if isinstance(blob, dict):
context.registered_actor_type = blob.get("type")
context.registered_actor_provider = blob.get("provider")
context.registered_actor_model = blob.get("model")
else:
context.registered_actor_type = None
context.registered_actor_provider = None
context.registered_actor_model = None
else:
context.registered_actor_type = None
context.registered_actor_provider = None
context.registered_actor_model = None
else:
assert isinstance(result.exception, SystemExit), (
f"Expected SystemExit, got: {result.exception}"
@@ -548,6 +564,8 @@ def step_run_actor_update(context: Context, name: str) -> None:
context.command_error = result.output
context.actor_registered = False
context.registered_actor_type = None
context.registered_actor_provider = None
context.registered_actor_model = None
# ── Then Steps (Assertions) — prefixed with "v3actor" to avoid conflicts ─────
@@ -1127,6 +1145,206 @@ route:
context.config_file = _write_yaml_file(context, "bad-edge-ref", yaml_content)
# ── Nested actors map: combined config.actor field (bug #11189) ───────────────
@given('a nested LLM actor YAML using combined config.actor field with name "{name}"')
def step_nested_llm_combined_actor(context: Context, name: str) -> None:
"""Create a spec-compliant nested-actors-map YAML using the combined config.actor field.
The ``actors:`` map format places ``type: llm`` at the actor-entry level
(``actors.<name>.type``), not inside the ``config:`` block. The provider
and model are supplied via the combined ``config.actor: provider/model``
shorthand rather than separate ``config.provider``/``config.model`` keys.
This is the exact format described in the spec and reproduced in bug #11189.
"""
yaml_content = f"""\
name: {name}
cleveragents:
version: "3.0"
default_actor: main
actors:
main:
type: llm
config:
actor: anthropic/claude-sonnet-4-5
system_prompt: "You are a test actor."
temperature: 0.3
"""
context.config_file = _write_yaml_file(context, name, yaml_content)
@given(
"a nested LLM actor YAML with explicit config.provider overriding config.actor"
' with name "{name}"'
)
def step_nested_llm_explicit_provider_overrides_combined(
context: Context, name: str
) -> None:
"""Create a nested-actors-map YAML with both config.provider and config.actor set.
When ``config.provider`` is explicitly present it must take precedence over
the provider parsed from the combined ``config.actor: provider/model``
shorthand. The model half of the combined field is still used as the
model fallback (since no separate ``config.model`` is provided here).
"""
yaml_content = f"""\
name: {name}
cleveragents:
version: "3.0"
default_actor: main
actors:
main:
type: llm
config:
provider: explicit-provider
actor: override/gpt-4
system_prompt: "Precedence test actor."
"""
context.config_file = _write_yaml_file(context, name, yaml_content)
@given('a nested LLM actor YAML with no provider or model fields with name "{name}"')
def step_nested_llm_no_provider_or_model(context: Context, name: str) -> None:
"""Create a nested-actors-map YAML with type: llm but no provider or model.
When neither ``config.provider``, ``config.model``, nor ``config.actor``
are present the extraction must fail gracefully and propagate a validation
error message so that users receive a clear diagnostic.
"""
yaml_content = f"""\
name: {name}
cleveragents:
version: "3.0"
default_actor: main
actors:
main:
type: llm
config:
system_prompt: "Actor without provider or model."
"""
context.config_file = _write_yaml_file(context, name, yaml_content)
@given(
"a nested LLM actor YAML with explicit config.model overriding config.actor"
' with name "{name}"'
)
def step_nested_llm_explicit_model_overrides_combined(
context: Context, name: str
) -> None:
"""Create a nested-actors-map YAML with both config.model and config.actor set.
When ``config.model`` is explicitly present it must take precedence over
the model parsed from the combined ``config.actor: provider/model``
shorthand. The provider half of the combined field is still used as the
provider fallback (since no separate ``config.provider`` is provided here).
"""
yaml_content = f"""\
name: {name}
cleveragents:
version: "3.0"
default_actor: main
actors:
main:
type: llm
config:
model: explicit-model
actor: anthropic/claude-sonnet-4-5
system_prompt: "Model precedence test actor."
"""
context.config_file = _write_yaml_file(context, name, yaml_content)
@given(
'a nested LLM actor YAML with combined config.actor "{combined_value}"'
' and name "{name}"'
)
def step_nested_llm_malformed_combined_actor(
context: Context, combined_value: str, name: str
) -> None:
"""Create a nested-actors-map YAML with a specific config.actor value.
Used to test edge cases of the combined ``config.actor: provider/model``
shorthand: empty model half (``provider/``), empty provider half
(``/model``), no delimiter (``noslash``), empty string, etc.
"""
yaml_content = f"""\
name: {name}
cleveragents:
version: "3.0"
default_actor: main
actors:
main:
type: llm
config:
actor: {combined_value}
system_prompt: "Malformed actor test."
"""
context.config_file = _write_yaml_file(context, name, yaml_content)
@then(
'v3actor the actor should be registered with provider "{provider}" and model "{model}"'
)
def step_actor_registered_with_provider_and_model(
context: Context, provider: str, model: str
) -> None:
"""Assert the actor was registered with the expected provider and model.
Inspects ``registered_actor_provider`` and ``registered_actor_model``
captured by the ``@when`` step from the ``config_blob`` passed to
``registry.upsert_actor()`` after CLI canonicalization. Canonicalization
calls ``setdefault`` so the canonical blob always carries the resolved
provider and model even when they were not present in the original YAML.
"""
assert context.actor_registered, (
"Actor was not registered. Output: "
f"{context.command_result.get('output', '') if context.command_result else ''}"
)
reg_provider = getattr(context, "registered_actor_provider", None)
reg_model = getattr(context, "registered_actor_model", None)
assert reg_provider == provider, (
f"Expected provider '{provider}' but got '{reg_provider}'"
)
assert reg_model == model, f"Expected model '{model}' but got '{reg_model}'"
@then('v3actor the actor should be registered with provider "{provider}"')
def step_actor_registered_with_provider(context: Context, provider: str) -> None:
"""Assert the actor was registered with the expected provider.
Checks ``registered_actor_provider`` captured by the ``@when`` step from
the ``config_blob`` passed to ``registry.upsert_actor()`` after
CLI canonicalization.
"""
assert context.actor_registered, (
"Actor was not registered. Output: "
f"{context.command_result.get('output', '') if context.command_result else ''}"
)
reg_provider = getattr(context, "registered_actor_provider", None)
assert reg_provider == provider, (
f"Expected provider '{provider}' but got '{reg_provider}'"
)
@then('v3actor the actor should be registered with model "{model}"')
def step_actor_registered_with_model(context: Context, model: str) -> None:
"""Assert the actor was registered with the expected model.
Checks ``registered_actor_model`` captured by the ``@when`` step from
the ``config_blob`` passed to ``registry.upsert_actor()`` after
CLI canonicalization.
"""
assert context.actor_registered, (
"Actor was not registered. Output: "
f"{context.command_result.get('output', '') if context.command_result else ''}"
)
reg_model = getattr(context, "registered_actor_model", None)
assert reg_model == model, f"Expected model '{model}' but got '{reg_model}'"
# ── Helper Functions ──────────────────────────────────────────────────────────
@@ -174,19 +174,6 @@ def step_node_sequence_negative(ctx: Context) -> None:
create_sequence_node(ctx, -1)
def create_sequence_node(ctx: Context, seq_val: int) -> None:
"""Create a single plan tree node with the given sequence value."""
ctx.tree_nodes = [
{
"decision_id": VALID_ULID_A,
"type": "strategy_choice",
"sequence": seq_val,
"question": "Q",
"children": [],
}
]
# ────────────────────────────────────────────────────────────
# Decision dict scenarios
# ────────────────────────────────────────────────────────────
@@ -404,3 +404,37 @@ Reject v3 GRAPH Actor With Unreachable Node
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 1
Should Contain ${result.stderr} unreachable
Register LLM Actor With Nested Actors Map And Combined config.actor Field
[Documentation]
... Verify that agents actor add accepts spec-compliant YAML using the
... nested actors map format with type at actor-entry level and provider/model
... supplied via the combined config.actor: "provider/model" shorthand.
... This is the regression test for bug #11189.
[Tags] slow
${yaml_file}= Set Variable ${TEMPDIR}${/}nested_combined_actor.yaml
${yaml_content}= Catenate SEPARATOR=\n
... name: local/nested-combined-llm
... cleveragents:
... ${SPACE}${SPACE}version: "3.0"
... ${SPACE}${SPACE}default_actor: main
... actors:
... ${SPACE}${SPACE}main:
... ${SPACE}${SPACE}${SPACE}${SPACE}type: llm
... ${SPACE}${SPACE}${SPACE}${SPACE}config:
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}actor: anthropic/claude-sonnet-4-5
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}system_prompt: "You are a test actor."
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}temperature: 0.3
Create File ${yaml_file} ${yaml_content}
${result}= Run Process ${PYTHON} ${HELPER} add local/nested-combined-llm ${yaml_file} cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-add-success
${show_result}= Run Process ${PYTHON} ${HELPER} show local/nested-combined-llm cwd=${WORKSPACE}
Log ${show_result.stdout}
Log ${show_result.stderr}
Should Be Equal As Integers ${show_result.rc} 0
Should Contain ${show_result.stdout} actor-show-success
Should Contain ${show_result.stdout} anthropic
Should Contain ${show_result.stdout} claude-sonnet-4-5
+85 -31
View File
@@ -12,6 +12,41 @@ from cleveragents.actor.yaml_loader import load_yaml_text
_V3_ACTOR_TYPES: frozenset[str] = frozenset({"llm", "graph", "tool"})
def _parse_combined_actor_field(data: dict[str, Any]) -> tuple[str | None, str | None]:
"""Extract (provider, model) from the combined ``config.actor`` shorthand.
Looks for ``config.actor: "provider/model"`` inside the first entry of
the nested ``actors:`` map. Returns ``(None, None)`` when the shorthand
is absent or malformed (empty provider or model half, missing delimiter).
Args:
data: Top-level actor YAML blob (may contain an ``actors:`` map).
Returns:
``(provider_value, model_value)`` both ``None`` when the combined
field is not present or cannot be parsed.
"""
actors_val = data.get("actors")
if not isinstance(actors_val, dict) or not actors_val:
return None, None
for _, first_entry in actors_val.items():
if not isinstance(first_entry, dict):
continue
config_block = first_entry.get("config")
if isinstance(config_block, dict):
combined = config_block.get("actor")
if isinstance(combined, str) and "/" in combined and combined.strip():
parts = combined.strip().split("/", 1)
provider = parts[0].strip()
model = parts[1].strip()
if provider and model:
return provider, model
break # Only examine the first dict entry in the actors map.
return None, None
class ActorConfiguration(BaseModel):
"""Canonical actor configuration parsed from user-provided blobs."""
@@ -159,12 +194,14 @@ class ActorConfiguration(BaseModel):
The v3 schema is identified by a top-level ``type`` key whose value
is one of ``llm``, ``graph``, or ``tool`` OR by a top-level
``actors:`` map containing at least one entry whose ``config:`` block
``actors:`` map containing at least one entry whose actor-entry level
carries a ``type`` field with one of those values.
Provider and model may appear at the top level OR nested inside
``actors.<actor-name>.config``. When found in the nested config,
they take precedence over top-level values.
``actors.<actor-name>.config`` (either as separate ``provider``/``model``
keys, or as a combined ``actor: "provider/model"`` shorthand). Explicit
``config.provider`` and ``config.model`` take precedence over the combined
shorthand when both are present.
For ``type: graph`` actors the ``route`` block is wrapped into a
graph descriptor.
@@ -179,17 +216,16 @@ class ActorConfiguration(BaseModel):
if isinstance(actors_val, dict) and actors_val:
for _, first_entry in actors_val.items():
if isinstance(first_entry, dict):
config_block = first_entry.get("config")
if isinstance(config_block, dict):
nested_type = config_block.get("type")
is_v3_type = (
isinstance(nested_type, str)
and nested_type.lower() in _V3_ACTOR_TYPES
)
if is_v3_type:
actor_type = nested_type
break
break
# The v3 spec places 'type' at the actor-entry level
# (sibling of 'config'), not inside the config block.
nested_type = first_entry.get("type")
is_v3_type = (
isinstance(nested_type, str)
and nested_type.lower() in _V3_ACTOR_TYPES
)
if is_v3_type:
actor_type = nested_type
break
if not isinstance(actor_type, str):
return None, None, None, False
@@ -197,8 +233,16 @@ class ActorConfiguration(BaseModel):
provider_value = data.get("provider")
unsafe_flag = bool(data.get("unsafe", False))
if not model_value or not isinstance(model_value, str):
if actor_type.lower() != "tool":
# Derive provider/model from nested actors map when top-level fields
# are absent. Separate config.provider / config.model keys take
# precedence over the combined config.actor shorthand.
if (not model_value or not isinstance(model_value, str)) or (
not provider_value or not isinstance(provider_value, str)
):
combined_provider, combined_model = _parse_combined_actor_field(data)
if not isinstance(model_value, str) or not model_value:
# First, check for a separate config.model key in the nested map.
model_from_separate: str | None = None
actors_val = data.get("actors")
if isinstance(actors_val, dict) and actors_val:
for _, first_entry in actors_val.items():
@@ -207,27 +251,37 @@ class ActorConfiguration(BaseModel):
if isinstance(config_block, dict):
mv = config_block.get("model")
if isinstance(mv, str) and mv:
model_value = mv
model_from_separate = mv
break
break
break
if model_from_separate:
model_value = model_from_separate
elif combined_model:
model_value = combined_model
if not isinstance(provider_value, str) or not provider_value:
# First, check for a separate config.provider key in the nested map.
provider_from_separate: str | None = None
actors_val = data.get("actors")
if isinstance(actors_val, dict) and actors_val:
for _, first_entry in actors_val.items():
if isinstance(first_entry, dict):
config_block = first_entry.get("config")
if isinstance(config_block, dict):
pv = config_block.get("provider")
if isinstance(pv, str) and pv:
provider_from_separate = pv
break
break
if provider_from_separate:
provider_value = provider_from_separate
elif combined_provider:
provider_value = combined_provider
if not model_value:
if actor_type.lower() != "tool":
return None, None, None, False
model_value = ""
if not provider_value or not isinstance(provider_value, str):
actors_val = data.get("actors")
if isinstance(actors_val, dict) and actors_val:
for _, first_entry in actors_val.items():
if isinstance(first_entry, dict):
config_block = first_entry.get("config")
if isinstance(config_block, dict):
pv = config_block.get("provider")
if isinstance(pv, str) and pv:
provider_value = pv
break
break
if not provider_value:
provider_value = (
infer_provider_from_model(model_value) if model_value else "custom"
+17
View File
@@ -112,6 +112,8 @@ class StdioTransport:
cwd=self._cwd,
)
except FileNotFoundError as exc:
self._process = None
from cleveragents.lsp.errors import LspError
raise LspError(
@@ -119,12 +121,27 @@ class StdioTransport:
details={"command": self._command, "args": self._args},
) from exc
except OSError as exc:
# Popen may have partially started the subprocess before
# raising (e.g. execve failure post-fork on some platforms).
# Ensure cleanup so the process does not leak into the caller's
# address space.
if self._process is not None:
self.stop()
self._process = None
from cleveragents.lsp.errors import LspError
raise LspError(
f"Failed to start LSP server: {exc}",
details={"command": self._command, "error": str(exc)},
) from exc
except Exception:
# Catch-all for any other low-level OSError / resource-error
# variants that might leave a zombie process behind.
if self._process is not None:
self.stop()
self._process = None
raise
logger.info(
"lsp.transport.started",