Skip to content

fix(orchestrator): auto-reply for terminal agent nodes#174

Merged
theuseless-ai merged 1 commit into
masterfrom
fix/auto-reply-terminal-agent
Mar 20, 2026
Merged

fix(orchestrator): auto-reply for terminal agent nodes#174
theuseless-ai merged 1 commit into
masterfrom
fix/auto-reply-terminal-agent

Conversation

@theuseless-ai

Copy link
Copy Markdown
Contributor

Summary

  • When a terminal agent node completes and the workflow has no reply_chat node, auto-promote its output to state["output"] so deliver() sends the chat response via the gateway
  • Fixes E2E chat round-trip for default-agent workflow (no reply_chat needed for standalone chat agents)
  • No behavior change for workflows that already use reply_chat or have downstream nodes after the agent

Design

Scenario Auto-reply?
[chat] → [agent] (terminal, no reply_chat) Yes
[chat] → [agent] → [switch] → ... (not terminal) No
[chat] → [agent] → [reply_chat] (explicit) No (reply_chat handles it)

Test plan

  • 4 new unit tests covering all scenarios (terminal agent, reply_chat exists, non-agent terminal, non-terminal agent)
  • E2E smoke test passes (20/20 with mock LLM)
  • Full test suite passes (2089 passed, 2 pre-existing failures unrelated)

🤖 Generated with Claude Code

…chat

When an agent node (agent/deep_agent) is terminal (no downstream
executable nodes) and the workflow has no explicit reply_chat node,
promote its output to state["output"] so _extract_output() → deliver()
sends the response back via the gateway.

This fixes the E2E chat round-trip for the default-agent workflow,
which has no reply_chat node but should still reply to the user.

Constraint: Only agent-type nodes trigger auto-reply — other terminal
  nodes (switch, code, etc.) do not produce user-facing responses
Rejected: Add reply_chat to every workflow template | couples template
  design to delivery mechanism
Confidence: high
Scope-risk: narrow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

I've Reviewed the Code

This change implements automatic output promotion for terminal agent nodes when no explicit reply_chat node exists, with comprehensive test coverage, but suffers from a state mutation side-effect that could cause double-save issues and minor terminology inconsistency in documentation.

⚠️ 2 issues found across 2 files

#1 Inconsistent terminology: 'reply_chat' vs 'chat reply'

platform/services/orchestrator.py L1036-L1042

The function name _auto_promote_agent_output and its docstring refer to 'reply_chat' node and 'chat reply', but this terminology is inconsistent. The code checks for component_type == "reply_chat" but the docstring says 'the agent's output is the intended chat reply'. This mixing of 'reply_chat' (a component type) and 'chat reply' (a description) creates confusion. The docstring should consistently use the technical term 'reply_chat' when referring to the node type.
Tags: language, maintainability
Affected code:

1036:     """Promote a terminal agent node's output to state["output"].
1037: 
1038:     When an agent node is terminal (no downstream executable nodes) and the
1039:     workflow has no explicit ``reply_chat`` node, the agent's output is the
1040:     intended chat reply.  Setting ``state["output"]`` ensures
1041:     ``_extract_output()`` finds it and ``deliver()`` sends it back.
1042:     """

Proposed change:

    """Promote a terminal agent node's output to state["output"].

    When an agent node is terminal (no downstream executable nodes) and the
    workflow has no explicit ``reply_chat`` node, the agent's output is the
    intended workflow output.  Setting ``state["output"]`` ensures
    ``_extract_output()`` finds it and ``deliver()`` sends it back.
    """

#2 State mutation side-effect during read-only check

platform/services/orchestrator.py L1030-L1065

The function _auto_promote_agent_output is called during _advance() which receives state as a parameter. The function modifies state["output"] and calls save_state() directly (line 1060-1061). However, _advance() also calls save_state() later indirectly through _check_loop_body_done(). This creates a race condition or double-save scenario where state could be saved multiple times in a single advancement flow, potentially causing issues with the Redis pipeline watch mechanism in save_state(). The function should either return the modified state for the caller to save, or document that it performs its own state persistence.
Tags: bug, architecture, maintainability
Affected code:

1030: def _auto_promote_agent_output(
1031:     completed_node_id: str,
1032:     state: dict,
1033:     topo_data: dict,
1034:     execution_id: str,
1035: ) -> None:
1036:     """Promote a terminal agent node's output to state["output"].
1037: 
1038:     When an agent node is terminal (no downstream executable nodes) and the
1039:     workflow has no explicit ``reply_chat`` node, the agent's output is the
1040:     intended chat reply.  Setting ``state["output"]`` ensures
1041:     ``_extract_output()`` finds it and ``deliver()`` sends it back.
1042:     """
1043:     if state.get("output") is not None:
1044:         return  # already set by another node
1045: 
1046:     node_info = topo_data["nodes"].get(completed_node_id)
1047:     if not node_info or node_info.get("component_type") not in _AGENT_COMPONENT_TYPES:
1048:         return
1049: 
1050:     has_reply_chat = any(
1051:         n.get("component_type") == "reply_chat"
1052:         for n in topo_data["nodes"].values()
1053:     )
1054:     if has_reply_chat:
1055:         return
1056: 
1057:     node_output = state.get("node_outputs", {}).get(completed_node_id, {})
1058:     output_text = node_output.get("output")
1059:     if output_text:
1060:         state["output"] = output_text
1061:         save_state(execution_id, state)
1062:         logger.info(
1063:             "Auto-promoted terminal agent %s output for execution %s",
1064:             completed_node_id, execution_id,
1065:         )

@theuseless-ai
theuseless-ai merged commit 10c69c1 into master Mar 20, 2026
4 checks passed
@codecov

codecov Bot commented Mar 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
platform/services/orchestrator.py 94.11% 1 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.

1 participant