diff --git a/pyagentspec/src/pyagentspec/adapters/langgraph/_node_execution.py b/pyagentspec/src/pyagentspec/adapters/langgraph/_node_execution.py index 4bcb1260..b2aa13b8 100644 --- a/pyagentspec/src/pyagentspec/adapters/langgraph/_node_execution.py +++ b/pyagentspec/src/pyagentspec/adapters/langgraph/_node_execution.py @@ -1067,9 +1067,17 @@ def is_single_string_output(expected_outputs: List[AgentSpecProperty]) -> bool: adapter takes it directly from the agent's final message rather than forcing structured generation. Mirrors ``LlmNodeExecutor``'s single-string handling and lets a string output work on models without structured-output support. + + An enum is excluded: its schema names the only values the output may take, so + free text cannot satisfy it. Treating it as free text left the allowed set + unenforced — the agent replied in prose and a branch keyed on the value + matched nothing. """ outputs = expected_outputs or [] - return len(outputs) == 1 and outputs[0].type == "string" + if len(outputs) != 1 or outputs[0].type != "string": + return False + schema = outputs[0].json_schema or {} + return not schema.get("enum") def extract_outputs_from_invoke_result( diff --git a/pyagentspec/tests/adapters/langgraph/flows/test_agentnode.py b/pyagentspec/tests/adapters/langgraph/flows/test_agentnode.py index 8930aede..afd53dce 100644 --- a/pyagentspec/tests/adapters/langgraph/flows/test_agentnode.py +++ b/pyagentspec/tests/adapters/langgraph/flows/test_agentnode.py @@ -107,6 +107,26 @@ def test_is_single_string_output() -> None: assert is_single_string_output([StringProperty(title="a"), StringProperty(title="b")]) is False +def test_a_single_enum_output_is_not_free_text() -> None: + """An enum names the only values its output may take, so it must go through + structured generation rather than being filled from the final message. + + Treated as free text, the allowed set was never enforced: the agent replied in + prose and a BranchingNode keyed on the value matched none of its paths. + """ + from pyagentspec.adapters.langgraph._node_execution import is_single_string_output + from pyagentspec.property import Property, StringProperty + + enum_output = Property( + title="sentiment", + json_schema={"type": "string", "enum": ["positive", "negative", "neutral"]}, + ) + + assert is_single_string_output([enum_output]) is False + # A plain string output keeps the free-text shortcut. + assert is_single_string_output([StringProperty(title="answer")]) is True + + def test_single_string_output_taken_from_final_message_without_structured_generation() -> None: """An AgentNode whose agent declares a single string output should resolve that output from the agent's final message — no structured generation, so