fix(compiler): thread actor-level system_prompt into graph node metadata #7

Merged
hurui200320 merged 1 commit from fix/compiler-thread-system-prompt into master 2026-05-22 07:08:20 +00:00
Member

Summary

compile_actor() silently dropped the actor-level system_prompt field for type: graph actors. The CompiledActor contained no trace of it, forcing host applications to keep the raw ActorConfigSchema in scope as a workaround.

This fix applies three targeted additions across two files to close both the compilation gap and the runtime context gap.

Motivation

docs/reference/actors_schema.md explicitly documents system_prompt as "Default prompt for graph-level agents" — placing it in the same semantic category as provider and model: actor-level fields that serve as fallback defaults for all AGENT nodes. The compiler already threaded provider and model via setdefault; system_prompt was simply never added to the same pattern.

A second independent gap in Node._execute_agent() meant that even after fixing the compiler, the system_prompt value would not appear in the context dict passed to agent.process_message().

Changes

src/cleveractors/actor/compiler.py

  • _map_node() gains an actor_system_prompt: str | None = None parameter.
  • merged_meta.setdefault("system_prompt", actor_system_prompt) is applied inside the if node.type == NodeType.AGENT: block, after the existing provider/model calls. setdefault preserves any per-node override in config.system_prompt.
  • Post-review fix: The key is only set when actor_system_prompt is not None, so actors without a top-level system_prompt don't carry a "system_prompt": None entry in every AGENT node's metadata.
  • compile_actor() passes actor_system_prompt=config.system_prompt to _map_node().

src/cleveractors/langgraph/nodes.py

  • Node._execute_agent() extracts node_system_prompt from self.config.metadata.get("system_prompt") and injects it into the context dict before state.metadata is merged. This allows host agents to read context.get("system_prompt") without a direct reference to the raw ActorConfigSchema. Per-execution state.metadata["system_prompt"] overrides still take precedence.
  • Post-review fix: A defensive isinstance(raw, str) guard rejects non-string values (e.g. integer, list) that could be injected via manually constructed NodeConfig.
  • Post-review fix: "system_prompt" is added to ignore_keys to prevent agents from mutating it in the context dict and accidentally persisting the mutation into metadata_updates.

docs/reference/actor_compiler.md

  • Node Binding table updated: AGENT row now references a detailed "AGENT node defaults" subsection describing the system_prompt threading behavior.

features/compiler_system_prompt.feature + features/steps/compiler_system_prompt_steps.py

Ten BDD scenarios covering the full behavioral surface:

  1. Actor-level system_prompt is threaded into each AGENT node's NodeConfig.metadata.
  2. Per-node config.system_prompt takes precedence over the actor-level default (setdefault semantics).
  3. agent.process_message() receives system_prompt in its context dict.
  4. state.metadata["system_prompt"] overrides the compiled default at runtime.
  5. state.metadata["system_prompt"] = None overrides a compiled default (clearing the prompt).
  6. Empty-string system_prompt is threaded into AGENT node metadata.
  7. Empty-string system_prompt is passed into the runtime context dict.
  8. Non-AGENT nodes (TOOL, CONDITIONAL, SUBGRAPH) do not receive system_prompt in compiled metadata.
  9. Actor without system_prompt does not add the key to AGENT node metadata (post-review: was "set to None").
  10. Absent system_prompt is excluded from the runtime context dict.

Post-review additions

Step-definition file reorganized into functional sections (compilation assertions, runtime assertions). "Each AGENT node" step definitions include a guard against zero-AGENT-node graphs.

Quality Gates

Gate Result
nox -s lint pass (0 issues)
nox -s typecheck pass (0 errors, 0 warnings)
nox -s unit_tests pass (17/17 scenarios)
nox -s coverage_report pass (≥ 97%)
nox -s security_scan pass (0 findings)
nox -s dead_code pass
nox -s complexity pass (avg complexity A)

Closes #6

## Summary `compile_actor()` silently dropped the actor-level `system_prompt` field for `type: graph` actors. The `CompiledActor` contained no trace of it, forcing host applications to keep the raw `ActorConfigSchema` in scope as a workaround. This fix applies three targeted additions across two files to close both the compilation gap and the runtime context gap. ## Motivation `docs/reference/actors_schema.md` explicitly documents `system_prompt` as _"Default prompt for graph-level agents"_ — placing it in the same semantic category as `provider` and `model`: actor-level fields that serve as fallback defaults for all AGENT nodes. The compiler already threaded `provider` and `model` via `setdefault`; `system_prompt` was simply never added to the same pattern. A second independent gap in `Node._execute_agent()` meant that even after fixing the compiler, the `system_prompt` value would not appear in the context dict passed to `agent.process_message()`. ## Changes ### `src/cleveractors/actor/compiler.py` - `_map_node()` gains an `actor_system_prompt: str | None = None` parameter. - `merged_meta.setdefault("system_prompt", actor_system_prompt)` is applied inside the `if node.type == NodeType.AGENT:` block, after the existing `provider`/`model` calls. `setdefault` preserves any per-node override in `config.system_prompt`. - **Post-review fix:** The key is only set when `actor_system_prompt is not None`, so actors without a top-level `system_prompt` don't carry a `"system_prompt": None` entry in every AGENT node's metadata. - `compile_actor()` passes `actor_system_prompt=config.system_prompt` to `_map_node()`. ### `src/cleveractors/langgraph/nodes.py` - `Node._execute_agent()` extracts `node_system_prompt` from `self.config.metadata.get("system_prompt")` and injects it into the context dict **before** `state.metadata` is merged. This allows host agents to read `context.get("system_prompt")` without a direct reference to the raw `ActorConfigSchema`. Per-execution `state.metadata["system_prompt"]` overrides still take precedence. - **Post-review fix:** A defensive `isinstance(raw, str)` guard rejects non-string values (e.g. integer, list) that could be injected via manually constructed `NodeConfig`. - **Post-review fix:** `"system_prompt"` is added to `ignore_keys` to prevent agents from mutating it in the context dict and accidentally persisting the mutation into `metadata_updates`. ### `docs/reference/actor_compiler.md` - Node Binding table updated: `AGENT` row now references a detailed "AGENT node defaults" subsection describing the `system_prompt` threading behavior. ### `features/compiler_system_prompt.feature` + `features/steps/compiler_system_prompt_steps.py` Ten BDD scenarios covering the full behavioral surface: 1. Actor-level `system_prompt` is threaded into each AGENT node's `NodeConfig.metadata`. 2. Per-node `config.system_prompt` takes precedence over the actor-level default (setdefault semantics). 3. `agent.process_message()` receives `system_prompt` in its context dict. 4. `state.metadata["system_prompt"]` overrides the compiled default at runtime. 5. `state.metadata["system_prompt"] = None` overrides a compiled default (clearing the prompt). 6. Empty-string `system_prompt` is threaded into AGENT node metadata. 7. Empty-string `system_prompt` is passed into the runtime context dict. 8. Non-AGENT nodes (TOOL, CONDITIONAL, SUBGRAPH) do **not** receive `system_prompt` in compiled metadata. 9. Actor without `system_prompt` does **not** add the key to AGENT node metadata (post-review: was "set to None"). 10. Absent `system_prompt` is excluded from the runtime context dict. ### Post-review additions Step-definition file reorganized into functional sections (compilation assertions, runtime assertions). "Each AGENT node" step definitions include a guard against zero-AGENT-node graphs. ## Quality Gates | Gate | Result | |------|--------| | `nox -s lint` | ✅ pass (0 issues) | | `nox -s typecheck` | ✅ pass (0 errors, 0 warnings) | | `nox -s unit_tests` | ✅ pass (17/17 scenarios) | | `nox -s coverage_report` | ✅ pass (≥ 97%) | | `nox -s security_scan` | ✅ pass (0 findings) | | `nox -s dead_code` | ✅ pass | | `nox -s complexity` | ✅ pass (avg complexity A) | Closes #6
hurui200320 force-pushed fix/compiler-thread-system-prompt from f4155e489d
Some checks failed
CI / unit_tests (pull_request) Successful in 43s
CI / typecheck (pull_request) Successful in 47s
CI / lint (pull_request) Failing after 53s
CI / build (pull_request) Has been skipped
CI / coverage (pull_request) Successful in 45s
CI / dead_code (pull_request) Successful in 29s
CI / security (pull_request) Successful in 43s
to a18e8c5931
All checks were successful
CI / lint (pull_request) Successful in 50s
CI / typecheck (pull_request) Successful in 47s
CI / unit_tests (pull_request) Successful in 46s
CI / coverage (pull_request) Successful in 48s
CI / security (pull_request) Successful in 43s
CI / dead_code (pull_request) Successful in 26s
CI / build (pull_request) Successful in 26s
2026-05-22 03:57:32 +00:00
Compare
hurui200320 force-pushed fix/compiler-thread-system-prompt from a18e8c5931
All checks were successful
CI / lint (pull_request) Successful in 50s
CI / typecheck (pull_request) Successful in 47s
CI / unit_tests (pull_request) Successful in 46s
CI / coverage (pull_request) Successful in 48s
CI / security (pull_request) Successful in 43s
CI / dead_code (pull_request) Successful in 26s
CI / build (pull_request) Successful in 26s
to bc01753113
Some checks failed
CI / dead_code (pull_request) Successful in 42s
CI / typecheck (pull_request) Successful in 1m13s
CI / security (pull_request) Successful in 1m12s
CI / coverage (pull_request) Successful in 1m13s
CI / unit_tests (pull_request) Successful in 1m19s
CI / lint (pull_request) Failing after 1m20s
CI / build (pull_request) Has been skipped
2026-05-22 04:30:06 +00:00
Compare
hurui200320 force-pushed fix/compiler-thread-system-prompt from bc01753113
Some checks failed
CI / dead_code (pull_request) Successful in 42s
CI / typecheck (pull_request) Successful in 1m13s
CI / security (pull_request) Successful in 1m12s
CI / coverage (pull_request) Successful in 1m13s
CI / unit_tests (pull_request) Successful in 1m19s
CI / lint (pull_request) Failing after 1m20s
CI / build (pull_request) Has been skipped
to 5aeadcc0aa
Some checks failed
CI / dead_code (pull_request) Successful in 43s
CI / typecheck (pull_request) Successful in 1m20s
CI / unit_tests (pull_request) Successful in 1m20s
CI / lint (pull_request) Failing after 1m21s
CI / build (pull_request) Has been skipped
CI / security (pull_request) Successful in 1m15s
CI / coverage (pull_request) Successful in 1m16s
2026-05-22 05:18:18 +00:00
Compare
hurui200320 force-pushed fix/compiler-thread-system-prompt from 5aeadcc0aa
Some checks failed
CI / dead_code (pull_request) Successful in 43s
CI / typecheck (pull_request) Successful in 1m20s
CI / unit_tests (pull_request) Successful in 1m20s
CI / lint (pull_request) Failing after 1m21s
CI / build (pull_request) Has been skipped
CI / security (pull_request) Successful in 1m15s
CI / coverage (pull_request) Successful in 1m16s
to 61c8a6f48f
All checks were successful
CI / dead_code (pull_request) Successful in 28s
CI / coverage (pull_request) Successful in 1m1s
CI / unit_tests (pull_request) Successful in 1m2s
CI / typecheck (pull_request) Successful in 1m11s
CI / security (pull_request) Successful in 1m9s
CI / lint (pull_request) Successful in 1m11s
CI / build (pull_request) Successful in 27s
2026-05-22 05:22:23 +00:00
Compare
hurui200320 force-pushed fix/compiler-thread-system-prompt from 61c8a6f48f
All checks were successful
CI / dead_code (pull_request) Successful in 28s
CI / coverage (pull_request) Successful in 1m1s
CI / unit_tests (pull_request) Successful in 1m2s
CI / typecheck (pull_request) Successful in 1m11s
CI / security (pull_request) Successful in 1m9s
CI / lint (pull_request) Successful in 1m11s
CI / build (pull_request) Successful in 27s
to dd6c20df5d
All checks were successful
CI / dead_code (pull_request) Successful in 43s
CI / typecheck (pull_request) Successful in 1m13s
CI / unit_tests (pull_request) Successful in 1m11s
CI / lint (pull_request) Successful in 1m16s
CI / security (pull_request) Successful in 1m15s
CI / coverage (pull_request) Successful in 1m16s
CI / build (pull_request) Successful in 26s
CI / typecheck (push) Successful in 1m3s
CI / unit_tests (push) Successful in 1m2s
CI / lint (push) Successful in 1m6s
CI / dead_code (push) Successful in 27s
CI / security (push) Successful in 57s
CI / coverage (push) Successful in 59s
CI / build (push) Successful in 37s
2026-05-22 06:05:27 +00:00
Compare
Author
Member

Self-QA Review — Approved

Ran 2 review/fix cycles. The PR is ready to merge.

Cycle 1 — Request Changes (0C / 3M / 5m / 2n)

All issues were addressed in the same cycle:

# Severity Issue Fix
M1 Major No test for empty-string system_prompt (compiler + runtime) 2 new BDD scenarios added
M2 Major No test for state.metadata = {"system_prompt": None} runtime override 1 new BDD scenario added
M3 Major CONDITIONAL and SUBGRAPH types untested (only TOOL was covered) Mixed-graph scenario extended
m1 Minor setdefault(None) unconditionally polluted AGENT node metadata shape Guarded with if actor_system_prompt is not None:
m2 Minor metadata.get() returned Any; no runtime type guard Replaced with isinstance(node_system_prompt, str)
m3 Minor system_prompt absent from ignore_keys — agent mutations leaked to state Added "system_prompt" to ignore_keys
m4 Minor # Then steps for new scenarios was a drafting artifact Reorganized into functional sections
m5 Minor "each AGENT node" loops tautologically passed on empty graphs agent_nodes_found > 0 guard added
n1 Nit AGENT doc table row was too dense De-densified + new subsection added

After fixes: 17/17 scenarios, all 7 quality gates

Cycle 2 — Approve (0C / 0M / 3m / 3n)

No further fixes applied. Remaining items are non-blocking:

# Severity Issue
m1 Minor No test for non-string non-None system_prompt in node metadata
m2 Minor No explicit test that ignore_keys exclusion works
m3 Minor No scenario for actor-None + per-node system_prompt override combination
n1 Nit Commit message body references stale scenario counts and guard semantics
n2 Nit _CapturingAgent uses shallow dict() copy instead of deepcopy
n3 Nit Gherkin step pattern fragile for quoted "None" edge case

These can be addressed in a follow-up ticket if desired.

Quality Gates (final)

Gate Status
nox -s lint Pass
nox -s typecheck Pass (0 errors)
nox -s unit_tests Pass (17/17)
nox -s coverage_report Pass (≥ 97%)
nox -s security_scan Pass (0 findings)
nox -s dead_code Pass
nox -s complexity Pass (avg A)
## Self-QA Review — Approved ✅ Ran 2 review/fix cycles. The PR is ready to merge. ### Cycle 1 — Request Changes (0C / 3M / 5m / 2n) All issues were addressed in the same cycle: | # | Severity | Issue | Fix | |---|----------|-------|-----| | M1 | Major | No test for empty-string `system_prompt` (compiler + runtime) | 2 new BDD scenarios added | | M2 | Major | No test for `state.metadata = {"system_prompt": None}` runtime override | 1 new BDD scenario added | | M3 | Major | CONDITIONAL and SUBGRAPH types untested (only TOOL was covered) | Mixed-graph scenario extended | | m1 | Minor | `setdefault(None)` unconditionally polluted AGENT node metadata shape | Guarded with `if actor_system_prompt is not None:` | | m2 | Minor | `metadata.get()` returned `Any`; no runtime type guard | Replaced with `isinstance(node_system_prompt, str)` | | m3 | Minor | `system_prompt` absent from `ignore_keys` — agent mutations leaked to state | Added `"system_prompt"` to `ignore_keys` | | m4 | Minor | `# Then steps for new scenarios` was a drafting artifact | Reorganized into functional sections | | m5 | Minor | "each AGENT node" loops tautologically passed on empty graphs | `agent_nodes_found > 0` guard added | | n1 | Nit | AGENT doc table row was too dense | De-densified + new subsection added | After fixes: **17/17 scenarios**, all 7 quality gates ✅ ### Cycle 2 — Approve (0C / 0M / 3m / 3n) No further fixes applied. Remaining items are non-blocking: | # | Severity | Issue | |---|----------|-------| | m1 | Minor | No test for non-string non-None `system_prompt` in node metadata | | m2 | Minor | No explicit test that `ignore_keys` exclusion works | | m3 | Minor | No scenario for actor-None + per-node `system_prompt` override combination | | n1 | Nit | Commit message body references stale scenario counts and guard semantics | | n2 | Nit | `_CapturingAgent` uses shallow `dict()` copy instead of `deepcopy` | | n3 | Nit | Gherkin step pattern fragile for quoted `"None"` edge case | These can be addressed in a follow-up ticket if desired. ### Quality Gates (final) | Gate | Status | |------|--------| | `nox -s lint` | ✅ Pass | | `nox -s typecheck` | ✅ Pass (0 errors) | | `nox -s unit_tests` | ✅ Pass (17/17) | | `nox -s coverage_report` | ✅ Pass (≥ 97%) | | `nox -s security_scan` | ✅ Pass (0 findings) | | `nox -s dead_code` | ✅ Pass | | `nox -s complexity` | ✅ Pass (avg A) |
hurui200320 deleted branch fix/compiler-thread-system-prompt 2026-05-22 07:08:20 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
cleveragents/cleveractors-core!7
No description provided.