compile_actor() drops graph-level system_prompt — not threaded into CompiledActor #6
Labels
No labels
auto/blocked-by-deps
auto/ci-timeout
auto/claimed-implementer
auto/claimed-merge
auto/claimed-reviewer
auto/driver-down
auto/invariant-violation
auto/last-attempt-tier-0
auto/last-attempt-tier-1
auto/last-attempt-tier-2
auto/last-attempt-tier-min
Automation Tracking
auto/needs-conflict-resolution
auto/needs-implementer
auto/postmortem
auto/ready-to-merge
auto/restart-throttled
auto/revert
auto/sentinel
auto/stale-inactivity
auto/unstable
Blocked
Bounty
$100
Bounty
$1000
Bounty
$10000
Bounty
$20
Bounty
$2000
Bounty
$250
Bounty
$50
Bounty
$500
Bounty
$5000
Bounty
$750
MoSCoW
Could have
MoSCoW
Must have
MoSCoW
Should have
Needs Feedback
Points
1
Points
13
Points
2
Points
21
Points
3
Points
34
Points
5
Points
55
Points
8
Points
88
Priority
Backlog
Priority
CI Blocker
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
State
Completed
State
Duplicate
State
In Progress
State
In Review
State
Paused
State
Unverified
State
Verified
State
Wont Do
Type
Automation
Type
Bug
Type
Discussion
Type
Documentation
Type
Epic
Type
Feature
Type
Legendary
Type
Refactor
Type
Support
Type
Task
Type
Testing
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Depends on
#7 fix(compiler): thread actor-level system_prompt into graph node metadata
cleveragents/cleveractors-core
Reference
cleveragents/cleveractors-core#6
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
compile_actor()silently drops the actor-levelsystem_promptfield fortype: graphactors. TheCompiledActorcontains no trace of it, forcing host applications to keep the rawActorConfigSchemain scope alongside the compiled bundle as a workaround.Metadata
fix(compiler): thread actor-level system_prompt into graph node metadatafix/compiler-thread-system-promptDetails
What the spec says
docs/reference/actors_schema.md(line 92) explicitly documentssystem_promptas:The word "default" places it in the same category as
providerandmodel: actor-level fields that serve as fallback defaults for all AGENT nodes in the graph.What the code does
actor/compiler.py:_map_node()already implements the default-threading pattern forproviderandmodel:_map_node()is never givenactor_system_promptand never writes it tomerged_meta. Aftercompile_actor()returns,system_promptis inaccessible fromCompiledActor.Additionally,
Node._execute_agent()mergesstate.metadatainto the context dict that reachesagent.process_message(), but it never includesself.config.metadata.get("system_prompt"). Even after the threading fix is applied to the compiler, hosts cannot read the system prompt from withinprocess_message()unless this second gap is also addressed.Impact
type: graphactors with a top-levelsystem_promptsilently lose it after compilation. The compiled bundle is incomplete relative to the schema.ActorConfigSchemain scope as a workaround, which defeats the purpose of storing a compiled bundle.Fix
Three targeted additions across two files:
actor/compiler.py—_map_node(): accept and threadactor_system_promptactor/compiler.py—compile_actor(): passconfig.system_promptthroughlanggraph/nodes.py—_execute_agent(): expose it in the context dictThe last change allows the host's
Agent.process_message()to readcontext.get("system_prompt")without needing a direct reference toNodeConfig. No changes toProviderRegistryPort.get()are required.Definition of Done
_map_node()accepts and threadsactor_system_promptinto AGENT nodemetadataviasetdefaultcompile_actor()passesconfig.system_promptto_map_node()Node._execute_agent()addsmetadata["system_prompt"]to the context dict when presentsystem_prompt→ compiledNodeConfig.metadatacontainssystem_promptprocess_message()receivessystem_promptin context dictdocs/reference/actor_compiler.mdupdated to document thatsystem_promptis threaded into node metadata alongsideproviderandmodelImplementation Notes
Branch:
fix/compiler-thread-system-promptRoot cause
actor/compiler.py:_map_node()already threadedproviderandmodelfrom the actor-level config into each AGENT node'smerged_metadict viasetdefault. Thesystem_promptfield was simply never added to the same pattern — a three-line omission.A second, independent gap in
langgraph/nodes.py:Node._execute_agent()meant that even after the compiler fix thesystem_promptvalue would not appear in the context dict passed toagent.process_message().Files changed
src/cleveractors/actor/compiler.py—_map_node()andcompile_actor()actor_system_prompt: str | None = Noneparameter to_map_node().merged_meta.setdefault("system_prompt", actor_system_prompt)inside theif node.type == NodeType.AGENT:block, directly after the existingprovider/modelsetdefault calls.compile_actor()to passactor_system_prompt=config.system_promptin the_map_node()call.setdefaultsemantics preserve any per-nodeconfig.system_promptoverride, exactly matching howproviderandmodelwork.src/cleveractors/langgraph/nodes.py—Node._execute_agent()node_system_prompt = self.config.metadata.get("system_prompt")before thecontext.update(state.metadata)call.context["system_prompt"] = node_system_promptso the host agent can read it fromcontext.get("system_prompt")without referencing the rawActorConfigSchema.state.metadataupdate so per-execution overrides viastate.metadata["system_prompt"]still take precedence.docs/reference/actor_compiler.md— Node Binding tableAGENTrow to document thatsystem_promptis now threaded alongsideproviderandmodel, and thatNode._execute_agent()injects it into the context dict.features/compiler_system_prompt.feature(new) — 3 BDD scenariossystem_promptis threaded into each AGENT node'smetadataon compile.system_promptin nodeconfigtakes precedence viasetdefaultsemantics.process_message()receivessystem_promptin its context dict when the node's metadata contains it.features/steps/compiler_system_prompt_steps.py(new) — step definitions_CapturingAgent: records the context dict received byprocess_message()._CapturingProviderRegistry: returns the same_CapturingAgentinstance for a given provider/model pair, allowing the test to inspect the received context post-execution.@when("the actor configuration is compiled")fromsmoke_steps.py(Behave loads all step files together).Quality gates
All gates pass on branch
fix/compiler-thread-system-prompt:nox -s lintnox -s typechecknox -s unit_testsnox -s coverage_reportnox -s security_scannox -s dead_codenox -s complexitySelf-QA Implementation Notes (Cycles 1–2)
Cycle 1
Review findings (0C / 3M / 5m / 2n — Request Changes):
system_prompt(compiler + runtime levels). The advertisedis not Nonefix had no regression guard.state.metadata = {"system_prompt": None}overriding a compiled default at runtime.setdefault("system_prompt", None)unconditionally inserted"system_prompt": Noneinto every AGENT node's metadata even when the actor had nosystem_prompt, silently changing metadata shape.metadata.get("system_prompt")returnsAny; thestr | Noneannotation was aspirational with no runtime guard.system_promptwas absent fromignore_keys, allowing agent mutations to leak intometadata_updates.# Then steps for new scenarioscomment was a drafting artifact.metadata.get()type-safety gap is a pre-existing pattern (tracked as future improvement, no action).Fixes applied:
compiler.py_map_node(): Guardedsetdefaultwithif actor_system_prompt is not None:so that actors without a top-levelsystem_promptno longer inject aNonekey into AGENT node metadata.nodes.py_execute_agent(): Replacedis not Nonewithisinstance(node_system_prompt, str)for defensive type safety; added"system_prompt"toignore_keys.compiler_system_prompt.feature: Scenario 6 updated to assert key absence (notNone); 3 new scenarios added for empty-string compile/runtime (M1) andNoneruntime override (M2).# Then steps — compilation assertions/# Then steps — runtime assertions);agent_nodes_foundguard added to all "each AGENT node" loops.All 7 quality gates passed: 17/17 scenarios (up from 14), coverage ≥ 97%.
Cycle 2
Review findings (0C / 0M / 3m / 3n — Approve):
system_promptin node metadata (theisinstanceguard's non-None branch).ignore_keyspreventssystem_promptfrom persisting tometadata_updates.None+ node-levelsystem_promptoverride (setdefault semantics when fallback is absent).isinstanceguard)._CapturingAgentuses shallow copy of context;deepcopywould be safer.@when('... containing system_prompt "{prompt}"')step pattern fragile for unquotedNone.Fixes applied:
None — Cycle 2 verdict was Approve. The remaining minor/nit findings do not affect correctness and the reviewer confirmed the PR is ready to merge.
Remaining Issues
The following items from Cycle 2 are open but non-blocking:
system_promptinjected viaNodeConfig.metadataignore_keysexclusion ofsystem_promptworkssystem_prompt+ per-node override combination_CapturingAgent.received_context = dict(context)should usedeepcopy"None"string vs unquotedNoneThese can be addressed as a follow-up in a separate ticket if desired. No action required before merge.
hurui200320 referenced this issue2026-06-03 06:48:39 +00:00