Support graph-level acp.command and acp.config defaults - #655
Conversation
Workflows that run the same ACP agent on several nodes had to repeat the process attribute on every node. `acp.command` and `acp.config` were read only from the node (`Node::acp_command_attr`), with no graph-level fallback. Add `AcpDefaultsTransform`, which materializes the graph-level value onto nodes before validation. The handler's `resolve_acp_process_spec` takes only a `Node`, so reading the graph at the use site (as `retry_target` does) would mean threading a `Graph` through the handler. As a transform, neither the handler nor `backend_valid` changes, and `fabro validate` and `fabro run` stay in agreement because both go through `pipeline::transform`. The two attributes are mutually exclusive, so they inherit as a pair: a node setting either one keeps its own and inherits neither. Only nodes with `backend="acp"` inherit, keeping the attributes off `start`/`exit` and API nodes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Notes for the reviewer — two judgment calls and one edge I left alone. Placed after A graph setting both attributes copies both. The result is the existing Not included: inferring Rejected alternative, for context: extending the stylesheet ( One pre-existing bug found while exploring, not fixed here: |
There was a problem hiding this comment.
Pull request overview
Adds graph-level defaults for ACP process configuration so multiple backend="acp" nodes can share a single acp.command or acp.config declaration, implemented as a workflow transform that materializes those defaults onto eligible nodes before validation/runtime.
Changes:
- Add graph-level accessors for
acp.command/acp.configand a newAcpDefaultsTransformthat applies inheritance rules (ACP-only, node-wins, pair suppression). - Wire the new transform into the workflow transform pipeline after imports/stylesheets so merged graphs inherit consistently.
- Add an integration test covering graph-level
acp.configacross multiple ACP nodes and update docs to describe the new DOT semantics.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| lib/foundation/fabro-types/src/graph.rs | Adds graph-level accessors for acp.command / acp.config. |
| lib/components/fabro-workflow/src/transforms/mod.rs | Registers and re-exports the new ACP defaults transform. |
| lib/components/fabro-workflow/src/transforms/acp_defaults.rs | Implements inheritance/materialization logic + unit tests. |
| lib/components/fabro-workflow/src/pipeline/transform.rs | Inserts AcpDefaultsTransform into the built-in pipeline. |
| lib/apps/fabro-cli/tests/it/workflow/acp.rs | Adds end-to-end test verifying graph-level ACP config is applied to multiple nodes. |
| docs/public/reference/dot-language.mdx | Documents graph-level defaults and node fallback behavior. |
| docs/public/core-concepts/agents.mdx | Adds user-facing guidance/example for sharing one ACP agent across nodes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let command = graph.acp_command_attr().map(ToString::to_string); | ||
| let config = graph.acp_config_attr().map(ToString::to_string); | ||
| if command.is_none() && config.is_none() { | ||
| return Ok(graph); | ||
| } | ||
|
|
||
| for node in graph.nodes.values_mut() { | ||
| if node.agent_backend() != Some(Ok(AgentBackend::Acp)) { | ||
| continue; | ||
| } | ||
| if node.acp_command_attr().is_some() || node.acp_config_attr().is_some() { | ||
| continue; | ||
| } | ||
|
|
||
| // A graph that sets both is ambiguous. Copy both so the existing | ||
| // "requires exactly one" check reports it, rather than silently | ||
| // picking one. |
Problem
Workflows that run the same ACP agent on several nodes had to repeat the process attribute on every node.
acp.commandandacp.configwere read only from the node (Node::acp_command_attr), with no graph-level fallback.Change
Set the agent once as a graph attribute; every
backend="acp"node inherits it.Both attributes are supported symmetrically, including the JSON
acp.configform.Semantics
acp.commandto its ownacp.configwithout inheriting a conflict.backend="acp"nodes inherit, keeping the attributes offstart/exitand API nodes.Implementation
AcpDefaultsTransformmaterializes the graph-level value onto nodes before validation, rather than reading the graph at the use site the wayretry_targetdoes. The handler'sresolve_acp_process_spectakes only aNode, so the read-at-use-site approach would mean threading aGraphthrough the handler. As a transform, neither the handler norbackend_validchanges, andfabro validateandfabro runagree by construction since both go throughpipeline::transform.Testing
workflow::acp::graph_level_acp_config_is_shared_by_every_acp_node— end-to-end run of the fake ACP agent from a graph-levelacp.configacross two nodes, which exercises the runtime path rather than just validationfabro-workflow,fabro-validate,fabro-types,fabro-cli; nightly fmt and clippy clean🤖 Generated with Claude Code