diff --git a/agent_actions/validation/static_analyzer/schema_extractor.py b/agent_actions/validation/static_analyzer/schema_extractor.py index 9074663..cae4160 100644 --- a/agent_actions/validation/static_analyzer/schema_extractor.py +++ b/agent_actions/validation/static_analyzer/schema_extractor.py @@ -319,8 +319,9 @@ def _extract_llm_schema( json_mode = config.get("json_mode", get_default("json_mode")) if not json_mode: output.is_schemaless = True + output_field = config.get("output_field", get_default("output_field")) + output.schema_fields.add(output_field) output.schema_fields.add("content") - output.schema_fields.add("raw_response") return output.is_schemaless = True diff --git a/tests/validation/static_analyzer/test_schema_extractor.py b/tests/validation/static_analyzer/test_schema_extractor.py index 30ae631..91b3dde 100644 --- a/tests/validation/static_analyzer/test_schema_extractor.py +++ b/tests/validation/static_analyzer/test_schema_extractor.py @@ -157,6 +157,35 @@ def test_nested_schema_properties(self): # Should NOT have nested fields at top level assert "name" not in schema.available_fields + def test_output_field_produces_named_field(self): + """Test that output_field declares the named field in schema_fields.""" + config = { + "name": "classify", + "json_mode": False, + "output_field": "issue_type", + "prompt": "Classify this issue", + } + schema = self.extractor.extract_schema(config) + + assert "issue_type" in schema.schema_fields + assert "issue_type" in schema.available_fields + assert "content" in schema.available_fields + # raw_response should NOT appear — output_field replaces it + assert "raw_response" not in schema.available_fields + + def test_non_json_without_output_field_defaults_to_raw_response(self): + """Regression guard: non-JSON mode without output_field still produces raw_response.""" + config = { + "name": "agent", + "json_mode": False, + "prompt": "Generate something", + } + schema = self.extractor.extract_schema(config) + + assert "raw_response" in schema.available_fields + assert "content" in schema.available_fields + assert schema.is_schemaless + def test_hitl_agent_uses_canonical_hitl_schema(self): """Test HITL action uses the canonical HITL output schema.""" config = { diff --git a/tests/validation/static_analyzer/test_workflow_static_analyzer.py b/tests/validation/static_analyzer/test_workflow_static_analyzer.py index 708c0e6..ff5c380 100644 --- a/tests/validation/static_analyzer/test_workflow_static_analyzer.py +++ b/tests/validation/static_analyzer/test_workflow_static_analyzer.py @@ -891,6 +891,34 @@ def test_context_scope_reports_error_when_schema_load_fails(self): "not found" in e.message or "could not be loaded" in e.message for e in context_errors ) + def test_output_field_observable_by_downstream(self): + """Downstream action can observe an output_field-produced field.""" + workflow_config = { + "actions": [ + { + "name": "classify", + "json_mode": False, + "output_field": "issue_type", + "prompt": "Classify", + }, + { + "name": "route", + "depends_on": ["classify"], + "context_scope": {"observe": ["classify.issue_type"]}, + "prompt": "Route based on {{ action.classify.issue_type }}", + "schema": { + "type": "object", + "properties": {"team": {"type": "string"}}, + }, + }, + ], + } + + result = analyze_workflow(workflow_config) + + errors = [e for e in result.errors if "issue_type" in e.message] + assert not errors, f"False positive on output_field: {errors}" + class TestPrimaryDependencyValidation: """Tests for primary_dependency validation."""