Skip to content

fix: preserve control ports for node group proxies - #5488

Open
mikemikimike wants to merge 7 commits into
griptape-ai:mainfrom
mikemikimike:fix/griptape-5487-flow-proxy-ports-v2
Open

mikemikimike wants to merge 7 commits into
griptape-ai:mainfrom
mikemikimike:fix/griptape-5487-flow-proxy-ports-v2

Conversation

@mikemikimike

Copy link
Copy Markdown

Summary

Preserve control-flow port semantics when a node-group boundary proxy is rebuilt through an add-parameter request.

Changes

  • Carry the original parameter type into node-group proxy requests.
  • Disable property mode for boundary proxies while retaining the input/output bridge needed by remapped connections.
  • Reconstruct control parameters as ControlParameterInput or ControlParameterOutput instead of a plain Parameter.
  • Preserve the bridge's opposite connection mode so both sides of the remapped connection remain valid.

Tests

  • uv run --group test python -m pytest tests/unit -q (492 passed, 1 xfailed)
  • uv run --group dev ruff check on changed files
  • uv run --group dev ruff format --check on changed files
  • uv run --group dev pyright on changed files
  • uv run --group dev typos on changed files
  • git diff --check

Issue

Closes #5487

@feltech feltech 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.

Welcome to the wonderful world of subflows 😟

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.

[fix] Manual test

  • Create 3x TextInput, chained together through Flow In->Flow Out
  • Create SubflowNodeGroup
  • Drag SubflowNodeGroup over the middle TextInput

Extra Flow In and Flow Out parameters are created.

Image

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.

Similar with ForEachGroup, though given there are multiple control parameters in this case, and I don't know much about how ForEachGroup is meant to work, I'm not actually sure what the correct control parameter is to pass through 😕 . I guess safest just to wire up Flow In/Out and users can re-target if they want.

Image

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.

Similarly with RetryGroup.

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.

[consider] It might be good to have an e2e test for this. I note that test_isolated_subflow_runs_data_only_graph is tangentially related, so could be augmented if suitable. Would be good to test after serialise/deserialise too.

# replayed (for example, while rebuilding a node-group boundary). A plain Parameter has
# the right type information but loses the control-port shape the editor and executor use.
if has_control_type:
input_shaped = (request.input_types is not None and request.output_type is None) or (

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.

[consider] This works in-memory, but not for deserialisation.

That is SubflowNodeGroup._create_proxy_parameter_for_connection emits a AddParameterToNodeRequest with either input_types or output_type being None. So that's good.

But Parameter.to_dict() serialises input_types and output_type as non-None in all cases. So when loading a saved workflow we go down the else branch below to make a bare ControlParameter.

This doesn't actually seem to cause a problem (though it's hard to tell due to other bug 🙄 #5563).

I suspect with #5563 fixed, we'd see control flow parameters rendering incorrectly, though.

# ControlParameter's convenience subclasses intentionally expose a small, purpose-
# built constructor. Apply the request-only lifecycle fields after construction so
# dynamic control parameters retain the same persistence semantics as data parameters.
new_param.allowed_modes = allowed_modes

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.

[consider][minor] allowed_modes is always {INPUT, OUTPUT}, even though technically the parameters should only be one or the other. But this doesn't cause a problem, and the UI prevents users wiring things incorrectly.

So I'd just suggest a little comment here explaining as much.

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.

Though also relates to #5488 (comment) - worth figuring out that first.

if input_shaped:
new_param = ControlParameterInput(
name=final_param_name,
tooltip=request.tooltip,

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.

[consider][minor] If request.tooltip=None then this splats the default tooltip (same for ControlParameterOutput)


if input_shaped:
new_param = ControlParameterInput(
name=final_param_name,

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.

[consider] Although the display name is fine, i.e. "Flow Out"/"Flow In" as expected, the actual parameter name is always exec_out_# (where # is an incrementing number), for both "Flow Out" and "Flow In" parameters.

You can see it e.g. in the sidebar Properties panel

Image

)

if input_shaped:
new_param = ControlParameterInput(

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.

[fix] The inner node is also not connected to the walls of the group, whereas pre-PR it was.

Image

I confirmed with sidebar agent (i.e. via MCP) that there are indeed internal connections to the Flow In/Out parameters, so they exist, but are just not rendered in the editor.

I'm actually not sure if we should have internal connections to the Flow In/Out. The subflow is like an independent mini-workflow, so the start and end of the flow are determined in the same way as a workflow (e.g. Start/End Flow nodes)... Something that needs a more knowledgeable person or some deep dive research to figure out.

@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.94737% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ptape_nodes/retained_mode/managers/node_manager.py 78.94% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

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.

Grouping a node with connected Flow In / Flow Out gives the group data ports instead of control ports

2 participants