Skip to content

Support graph-level acp.command and acp.config defaults - #655

Draft
brynary wants to merge 1 commit into
mainfrom
feat/graph-level-acp-defaults
Draft

Support graph-level acp.command and acp.config defaults#655
brynary wants to merge 1 commit into
mainfrom
feat/graph-level-acp-defaults

Conversation

@brynary

@brynary brynary commented Jul 27, 2026

Copy link
Copy Markdown
Member

Problem

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.

Change

Set the agent once as a graph attribute; every backend="acp" node inherits it.

graph [acp.command="python3 tools/fake_acp_agent.py"]

plan      [label="Plan",      backend="acp"]
implement [label="Implement", backend="acp"]
review    [label="Review",    backend="acp", acp.command="python3 tools/reviewer.py"]

Both attributes are supported symmetrically, including the JSON acp.config form.

Semantics

  • Inherited as a pair. The two attributes are mutually exclusive, so a node setting either one keeps its own and inherits neither. A node can switch from a shared acp.command to its own acp.config without inheriting a conflict.
  • Only backend="acp" nodes inherit, keeping the attributes off start/exit and API nodes.
  • Node value wins over the graph value.

Implementation

AcpDefaultsTransform materializes the graph-level value onto nodes before validation, rather than reading the graph at the use site the way retry_target does. The handler's resolve_acp_process_spec takes only a Node, so the read-at-use-site approach would mean threading a Graph through the handler. As a transform, neither the handler nor backend_valid changes, and fabro validate and fabro run agree by construction since both go through pipeline::transform.

Testing

  • 6 unit tests on the transform: inheritance for each attribute, node-wins, the pair rule, non-ACP nodes untouched, no-op when the graph sets nothing
  • workflow::acp::graph_level_acp_config_is_shared_by_every_acp_node — end-to-end run of the fake ACP agent from a graph-level acp.config across two nodes, which exercises the runtime path rather than just validation
  • 2771 tests pass across fabro-workflow, fabro-validate, fabro-types, fabro-cli; nightly fmt and clippy clean

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings July 27, 2026 18:59
@brynary

brynary commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

Notes for the reviewer — two judgment calls and one edge I left alone.

Placed after ImportTransform. Imported nodes inherit the parent graph's default like any other node in the merged graph. This makes graph-level defaults a second, wider path to imported nodes alongside the existing placeholder propagation (imports.mdx lists acp.command/acp.config as propagating attributes). Moving the transform earlier would keep imports self-contained instead. I picked the current order because "graph-level default applies to the nodes in the graph" is the simpler rule to explain, but it's a one-line move.

A graph setting both attributes copies both. The result is the existing requires exactly one of acp.command or acp.config error firing once per inheriting node, rather than once at the graph level. Correct but noisy. A graph-level lint rule would report it in one place; I left that out as it's beyond the change under discussion.

Not included: inferring backend="acp" from the presence of either attribute. That would remove the other repeated attribute, and it's safe — backend_valid only fires when backend is set (backend_valid.rs:21), so acp.command alone is inert today and nothing depends on that. Separate change if wanted.

Rejected alternative, for context: extending the stylesheet (STYLESHEET_PROPERTIES already carries backend) reads like the natural home, but stylesheet values terminate at ; or } with no quoting, so every valid acp.config truncates — it fails at the stylesheet parse before reaching ACP code. Supporting one attribute there and not the other would be worse than supporting neither.

One pre-existing bug found while exploring, not fixed here: * { model: ... } combined with a class-based backend: acp rule poisons the ACP nodes, because the universal rule writes model/provider onto them and backend_valid rejects API-only attributes on ACP nodes. Reproduces on main without any of this change. The stylesheet pass should skip API-only properties on ACP-backed nodes. Happy to file it separately.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.config and a new AcpDefaultsTransform that 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.config across 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.

Comment on lines +25 to +41
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants